&lt;?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0"
     xmlns:atom="http://www.w3.org/2005/Atom"
     xmlns:content="http://purl.org/rss/1.0/modules/content/"
     xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>GolfNext Engineering</title>
    <link>https://build.golfnext.com/en/</link>
    <description>Engineering, operations, and infrastructure at a small, ambitious team.</description>
    <language>en-US</language>
    <copyright>© 2026 GolfNext ApS · Ry, Denmark</copyright><lastBuildDate>Wed, 14 Oct 2026 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://build.golfnext.com/en/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>AI unleashed</title>
      <link>https://build.golfnext.com/en/posts/ai-unleashed/</link>
      <pubDate>Wed, 14 Oct 2026 00:00:00 +0000</pubDate><dc:creator>John Allan Nielsen</dc:creator>
      <guid>https://build.golfnext.com/en/posts/ai-unleashed/</guid>
      <description>In August, AI to me was autocomplete and a chat window, and I wasn&#39;t impressed. Now I hand plans to up to thirty agents at a time, on a backend merge that isn&#39;t allowed any downtime. The agents only run loose because the plan is tight: how I decide what to hand over, why a couple of hours of planning is the expensive part, and what the review caught when the agents wrote Java in Go.</description>
      <content:encoded><![CDATA[<p>I wasn&rsquo;t AI native when I started at GolfNext in August. I had used GitHub Copilot before, both the autocomplete that suggests the next few lines while you type and the chat where you can prompt it, and I wasn&rsquo;t impressed. It couldn&rsquo;t do my tasks for me. At the time I was building an integration against a system whose documentation was private, behind a login, and there was no way to make Copilot read it. So whether it was completing a line or answering a prompt, it was working against an API it had never seen, and I did the actual work myself.</p>
<p>My boss told me I&rsquo;d be amazed by Claude Code. I thought I&rsquo;d believe that when I saw it. Then I saw it.</p>
<p>Claude Code isn&rsquo;t autocomplete, and it isn&rsquo;t a chat window either. It&rsquo;s an agent: you give it a task, and it reads the code, runs commands, makes changes and checks its own work. It can even use my browser when I let it, so documentation behind a login is something it can go and read, the same way I would. The wall I hit with Copilot simply isn&rsquo;t there anymore. In the two months since, the way I work has changed shape around that. These days I&rsquo;d call it agentic engineering. I spend my time deciding what should be built and checking that it was, and agents do most of the building in between.</p>
<p>Here&rsquo;s what that looks like on the biggest thing I&rsquo;m working on.</p>
<p>We have two admin portals. Each has its own backend, its own login and its own database, and club staff who use both log in twice. Merging them into one is on the roadmap, and I&rsquo;ve <a href="/en/posts/road-to-glory/">written before</a> about the order we chose: a bridge between the two logins first, then one frontend over both backends, then the backends themselves.</p>
<p>The first two steps are done. The frontend of one portal now lives inside the other, the two platforms talk to each other, and feature development carried on in both the whole time. Nobody got a maintenance window. Golf clubs run on this software every day, so there was no planned downtime, and there won&rsquo;t be any for the rest of it either.</p>
<p>The next step is harder. One of the backends is written in Java, and we&rsquo;re rebuilding it in Go. Once that&rsquo;s done, one backend gets ported into the other, and we end up with a single application. Again without stopping feature work, and again without a window.</p>
<p>Most of the code for that won&rsquo;t be typed by a person. That&rsquo;s the part people ask about, and it&rsquo;s the least interesting part. The first decision on any piece of work is its size, because I use agents in three different ways. The backend merge is the large case, and I&rsquo;ll get there by way of the small and medium ones. What changes between the three is how much of the <em>thinking</em> I hand over, and when I take it back.</p>
<h2 id="small-can-you-do-this-on-your-own">Small: can you do this on your own?<a class="gn-heading-anchor" href="#small-can-you-do-this-on-your-own" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>For a small task I ask the agent one question before anything else: can you do this on your own?</p>
<p>I don&rsquo;t say &ldquo;do this&rdquo;. I ask whether it can. It reads the code, looks at what the task touches, and tells me whether it&rsquo;s confident or whether there&rsquo;s something it doesn&rsquo;t know. Most of the time the answer is yes, and it does it. Sometimes the answer is &ldquo;this depends on how X behaves and I can&rsquo;t see that from here&rdquo;, which is exactly the sentence I want to hear before it starts rather than after.</p>
<p>A typical one: a bug report about a printer that doesn&rsquo;t print. The software knew perfectly well something had gone wrong, because it wrote the error to the logs. It just never told the person using it, who got no warning at all and was left waiting for a printout that wasn&rsquo;t coming. The fix is small and well-bounded: find where the error is caught, and show it to the user instead of only logging it. I asked the question, got a yes, and the agent fixed it on its own.</p>
<p>The question costs nothing, and it changes the failure mode. An agent told to do something will do something. An agent asked whether it can is allowed to say no.</p>
<h2 id="medium-research-first-options-second">Medium: research first, options second<a class="gn-heading-anchor" href="#medium-research-first-options-second" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>For a medium task the agent goes first, but it doesn&rsquo;t build anything. It evaluates the task, does the research (the code involved, the API documentation, how the same problem is already solved elsewhere in our repositories) and comes back with what it found and what it would suggest.</p>
<p>A typical one: a feature request from a club that uses a specific integration. The agent can read the integration, see what it already supports and work out where the request would fit. But it comes back with questions rather than an answer. Is this feasible at all with what the integration exposes? If it is, which part of the integration should be expanded? Or is this one club&rsquo;s need different enough that it deserves a custom module of its own, rather than bending a shared integration around it?</p>
<p>Those aren&rsquo;t questions the code can answer. They depend on how many other clubs use that integration, what we&rsquo;re willing to maintain, and where the product is heading, and none of that is written down anywhere the agent can read. So it can&rsquo;t complete the task without me, and it shouldn&rsquo;t pretend it can. What it can do is turn a vague request into a short list of real choices with the trade-offs worked out. Then I start shaping: I pick one, combine two, or throw them all out because the research changed the question.</p>
<h2 id="large-a-couple-of-hours-before-any-code">Large: a couple of hours, before any code<a class="gn-heading-anchor" href="#large-a-couple-of-hours-before-any-code" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>For a large project, and merging two backends while both of them keep shipping features is a large project, I spend a couple of hours planning with the agent before anything gets built.</p>
<p>Part of that is brainstorming. Most of it is me supplying context the agent cannot get on its own:</p>
<ul>
<li><strong>Architecture.</strong> What talks to what, which boundaries have to hold, what the target shape is and which compromises are allowed on the way there.</li>
<li><strong>Technology.</strong> What we&rsquo;re standardising on, what we&rsquo;re moving away from, and what is off the table.</li>
<li><strong>Purpose.</strong> Why we&rsquo;re doing this at all. Here: club staff should log in once, and we should maintain one system instead of two.</li>
<li><strong>User experience.</strong> What the person at the other end will see during every step of the migration, including the steps that are only halfway done.</li>
</ul>
<p>The plan starts its life in the agent&rsquo;s context, the running conversation we&rsquo;ve just spent two hours filling. That&rsquo;s a fragile place to keep anything. Close the session and it&rsquo;s gone, and nobody else on the team can see it while it lasts.</p>
<p>So the last thing the planning session does is write it out, as tasks in our task system rather than a long document. Each task gets its subtasks, and the agent records the dependencies between them: what has to exist before a task can start, and what is waiting for it to finish. By the time I close the session, the plan has stopped being a conversation and become something the rest of the team can see, pick up and track.</p>
<p>Before anything runs, the team hears it. We talk it through briefly, and they hear the suggested solution. Those talks are short, because we already agree on most of the direction. Sometimes I also ask a colleague to look at the plan itself for a second opinion, since a plan that only one person and one agent have seen has had exactly one human check.</p>
<p>This is the expensive part, and it should be. Every question answered here is a question an agent won&rsquo;t have to guess at later. Every dependency written down is a collision that won&rsquo;t happen when thirty things are running at once.</p>
<h2 id="execution-is-the-boring-part-on-purpose">Execution is the boring part, on purpose<a class="gn-heading-anchor" href="#execution-is-the-boring-part-on-purpose" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Once the plan exists, executing it is almost an anticlimax. I hand it back to the agent, and it starts subagents (separate agents it launches and coordinates itself, each with its own task) on every subtask whose dependencies are met. Usually somewhere between ten and thirty of them at a time. When one finishes, whatever was waiting for it can start.</p>
<p>Thirty agents in one codebase sounds like a recipe for them tripping over each other. What keeps them apart is the plan. The dependencies written down during planning decide what can run at the same time and what has to wait, so two agents don&rsquo;t end up building on something that doesn&rsquo;t exist yet.</p>
<p>This works because nothing that matters is left for the agents to decide. An agent running a well-defined subtask with its context written down next to it is doing exactly the kind of work agents are good at. An agent running a vague task guesses fluently, and you find out which parts were guesses afterwards.</p>
<p>That&rsquo;s also why I don&rsquo;t trust the word &ldquo;unleashed&rdquo; when people use it about AI. The agents only run loose because the plan is tight.</p>
<h2 id="did-they-build-what-i-planned">Did they build what I planned?<a class="gn-heading-anchor" href="#did-they-build-what-i-planned" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>When all the agents are done, I first check whether they built what I planned. Did every task actually land, and does the whole add up to the thing we designed, rather than a set of subtasks that each pass their own tests?</p>
<p>Then I review from three directions:</p>
<ul>
<li><strong>Architecture.</strong> Did the boundaries hold? Did anything reach across a seam it shouldn&rsquo;t, or quietly add a dependency the plan didn&rsquo;t have?</li>
<li><strong>Code quality.</strong> Is this code someone on the team can own next year? Consistent with the rest of the repository, tested where it matters, no clever parts nobody asked for.</li>
<li><strong>User experience.</strong> Does it feel right to the person using it, including mid-migration, when half of the system is old and half is new?</li>
</ul>
<p>Two of those, architecture and user experience, are context I gave the agent at the start, so the review is really checking the work against what I supplied. If it keeps finding the same kind of problem, the fix usually belongs in the next plan.</p>
<p>The clearest catch so far came from the backend rewrite itself, a port from Java to Go. On paper, the agents delivered what was planned: the endpoints were there and did what the old ones did. But the first version was Java written in Go. Where the Java code leaned on a library, the agents rebuilt that library in Go by hand, even when Go&rsquo;s own standard library already did the job. Dates were the obvious case. Instead of using Go&rsquo;s <code>time</code> package, which handles them perfectly well, we got a home-made copy of how Java does date and time.</p>
<p>None of that fails a test. It compiles, it runs, it returns the right answers. It&rsquo;s also code nobody on the team should have to maintain: more of it than needed, shaped like a language it isn&rsquo;t written in, and unfamiliar to anyone who knows Go. That&rsquo;s a code quality problem and an architecture problem at once, and you only catch it by asking &ldquo;is this how we&rsquo;d build it?&rdquo; as well as &ldquo;does it work?&rdquo;.</p>
<p>The fix went back through the same loop as everything else. I asked Claude how we could get rid of the home-made library and use Go&rsquo;s standard one instead, without losing anything. That last part was the real constraint. Clients already call those endpoints, and every timestamp they get back has to come out in exactly the same format after the port as before it. A port that is cleaner on the inside and breaks somebody&rsquo;s date parsing on the outside isn&rsquo;t finished. It got fixed: the home-made library went, and the timestamps stayed the same.</p>
<h2 id="where-my-time-went">Where my time went<a class="gn-heading-anchor" href="#where-my-time-went" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Add it up, and on a large project the typing has nearly disappeared from my day. What&rsquo;s left is the two ends: hours of planning at the start, and a careful review at the end. It&rsquo;s different work, and there&rsquo;s no less of it. But I get far more done with it.</p>
<p>I think that&rsquo;s the right way to see AI coding: as a compiler, not as something coming for our jobs. Nobody writes machine code by hand anymore. The compiler took that over decades ago, and it didn&rsquo;t make programmers redundant. It moved the work up a level, to deciding what the program should do. Agents are the next step up, with one big difference: a compiler doesn&rsquo;t guess. That&rsquo;s why the review at the end isn&rsquo;t optional.</p>
<p>So far, here, the productivity hasn&rsquo;t turned into fewer people. It&rsquo;s turned into bigger ambitions. Every team has a list of things clearly worth doing that never beat the next customer request: better internal tooling, features customers would enjoy but never asked for loudly, easier onboarding for the next person who joins. Now we have time for them.</p>
<p>In August, AI to me was autocomplete and a chat window, and I wasn&rsquo;t impressed. Two months later I&rsquo;m planning the merge of two backends that can&rsquo;t be taken down, and handing the plan to 10, 20 or 30 agents at a time.</p>
<p>My boss was right. I was amazed. But the speed of the typing isn&rsquo;t what amazed me. It was finding out that once the typing stops being the job, the part that&rsquo;s left, the plan and the review, is the part I&rsquo;d always rather have been doing.</p>
]]></content:encoded>
      <category>AI</category>
      <category>Engineering</category>
      <category>Architecture</category>
    </item>
    <item>
      <title>Feeling like a hot chick</title>
      <link>https://build.golfnext.com/en/posts/feeling-like-a-hot-chick/</link>
      <pubDate>Thu, 08 Oct 2026 00:00:00 +0000</pubDate><dc:creator>John Allan Nielsen</dc:creator>
      <guid>https://build.golfnext.com/en/posts/feeling-like-a-hot-chick/</guid>
      <description>At the end of September a club signed with us almost without being sold to. Its members had used our kiosks at other venues and came home asking &#34;Why not here?&#34; This post is about what that does to you as an engineer: working on the product everyone wants, and why that makes me want to do my best work.</description>
      <content:encoded><![CDATA[<p>At the end of September one of our sales managers closed a deal that took almost no time. Peter Selmer, who founded GolfNext in 2012, <a href="https://www.linkedin.com/posts/peterselmer_golf-golfindustry-golftech-share-7510606974385000451-XUrx/">wrote about it on LinkedIn</a>. The sales manager didn&rsquo;t have to convince anyone. The club&rsquo;s members had already done it. They&rsquo;d used our kiosks at other golf clubs, then came home and asked their own club: &ldquo;Why not here?&rdquo;</p>
<p>I commented under the post that golfers asking their own club for GolfNext is the best signal there is. I&rsquo;ve kept thinking about it since, and this post is the longer version of that comment.</p>
<h2 id="the-one-everyone-wants">The one everyone wants<a class="gn-heading-anchor" href="#the-one-everyone-wants" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>So, the title. The hot chick in this story isn&rsquo;t me. It&rsquo;s the product.</p>
<p>GolfNext makes software that golf clubs run on, and the part golfers see most is the self-service kiosk: a digital reception that runs around the clock, so a player can pay a green fee, rent equipment or buy from the pro shop without anyone standing behind a counter. As far as we know, nothing else like it exists in golf.</p>
<p>Most software only gets noticed when it breaks. Nobody walks into a building and asks who made the door handles. But golfers use our kiosk at one club, and then they go to the next club and miss it. Then they ask for it. That&rsquo;s what being attractive looks like for a product: people want it before anybody has pitched it to them.</p>
<p>Peter put the shift in one line: golfers now expect check-in to be fast and simple, and they notice when it isn&rsquo;t.</p>
<h2 id="what-it-does-to-me">What it does to me<a class="gn-heading-anchor" href="#what-it-does-to-me" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>The kiosk is something we make ourselves. Colleagues in-house build part of the hardware, and my other colleagues and I build the software that runs on it and everything around it. So when golfers ask for it by name, they&rsquo;re asking for something we made, and I&rsquo;d be lying if I said I didn&rsquo;t feel a bit of that attraction rub off.</p>
<p>It&rsquo;s a good feeling, and it does something to how I work. When you know people are asking for the thing you make, by name, you want to be worthy of it. A bug that would be a backlog item somewhere else stops being one. A screen that&rsquo;s fine becomes a screen I&rsquo;d like to be great. You start reasoning from the player standing at the kiosk at half past five in the morning, because that player is the one who&rsquo;ll go home and tell their own club about it. Or won&rsquo;t.</p>
<p>Nobody has to tell me to do my best on a product like that. I want to.</p>
<h2 id="staying-attractive-is-daily-work">Staying attractive is daily work<a class="gn-heading-anchor" href="#staying-attractive-is-daily-work" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Products don&rsquo;t stay attractive by themselves. We work on it continuously, and most of it comes down to four things.</p>
<p><strong>Easy to use.</strong> For the player at the kiosk, and for the staff behind the desk. When a PDF of ball codes that clubs print out started spilling onto two pages, it didn&rsquo;t wait in a backlog for a month. It was fixed within the hour. Small things like that are the difference between a product people put up with and one they ask for.</p>
<p><strong>Easy to integrate.</strong> Clubs already run other systems, and they shouldn&rsquo;t have to choose between those and us. The easier we are to plug in next to what a club already has, the fewer reasons it has to say &ldquo;not yet&rdquo;.</p>
<p><strong>Less dull manual work.</strong> Every physical device we have out there, kiosk screens, card terminals, ball dispensers, now joins with a 12-digit code typed in on site, and from then on gets its settings and updates from us. Nobody connects to a device and sets it up by hand anymore. The next step is a small web page where club staff set up a new device themselves, so replacing a broken one takes a parcel and a link instead of several phone calls. Nobody went into this job to type the same settings into the fortieth machine, and nobody at a club did either.</p>
<p><strong>Open when everyone has gone home.</strong> This is the one that matters most to me. The kiosk is a reception that doesn&rsquo;t close. When the last employee has locked up and driven home, a player can still turn up, pay a green fee and play. The club keeps earning outside opening times, and the golfer gets the same convenience they have everywhere else in their life. That only works if the product is reliable when there&rsquo;s nobody on site to restart it.</p>
<p>And then there&rsquo;s listening. When a club tells us something is a bug, we usually solve it the same day. Sometimes, though, the thing one club calls a bug is exactly what another club likes. Then we don&rsquo;t pick a winner. We make it a setting, and each club decides for itself. Everybody stays happy, and club administrators stay in charge of their own setup instead of having to ask us to change it for them. A product that&rsquo;s self-service for golfers should be self-service for the people running the club too.</p>
<h2 id="where-its-heading">Where it&rsquo;s heading<a class="gn-heading-anchor" href="#where-its-heading" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>None of this is about being a nice product for a handful of clubs. In August I wrote <a href="/en/posts/road-to-glory/">Road to glory</a>, our plan for the platform underneath everything, and I said the ambition out loud there: we want this to be the platform the golf industry runs on. Every club, every range, every country that has both.</p>
<p>That post was about the engine: fewer languages, one shape for every repository, a codebase anyone on the team can open and be useful in. I also wrote that the engine can&rsquo;t create the thing that makes the ambition realistic. Clubs who see the product are amazed by it, and that reaction is the asset. Peter&rsquo;s Friday deal is that reaction, playing out in a sales meeting.</p>
<p>So the engine matters because of what it lets us do for the people standing in front of the screen. And for that, user experience comes first. Every decision starts from the person using it: the golfer at the kiosk before the first tee time, the staff member who has to explain something to a member, the administrator changing a setting at the end of a long day. If a feature is clever but makes any of them stop and think, it isn&rsquo;t finished. The best compliment our product can get is that nobody notices it, right up until they&rsquo;re at a club that doesn&rsquo;t have it.</p>
<p>That&rsquo;s the kind of ambition that keeps me going. Not just to grow, but to grow by being the thing people prefer.</p>
<h2 id="attractive-is-a-promise">Attractive is a promise<a class="gn-heading-anchor" href="#attractive-is-a-promise" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>There&rsquo;s a flip side, and I think it&rsquo;s the important part.</p>
<p>Being the one everyone wants raises the bar. Members who compare clubs will compare us too, and a kiosk that&rsquo;s frozen at half past five in the morning isn&rsquo;t just a bug. It&rsquo;s the moment someone stops asking &ldquo;why not here?&rdquo;. The attraction only lasts as long as the product keeps earning it.</p>
<p>That&rsquo;s what the last part of my comment was about. &ldquo;We will keep delivering&rdquo; sounds like the kind of thing you write under a colleague&rsquo;s post. I meant it as a promise. The sales team can only close deals that fast because golfers already like what they&rsquo;ve used, and keeping it that way is my job.</p>
<p>It&rsquo;s a nice job to have. Most days I&rsquo;m just an engineer fixing a print layout or chasing a bug. But when golfers go home and ask their club for the thing we make, I&rsquo;ll admit it: for a moment, I&rsquo;m feeling like a hot chick. And there&rsquo;s nothing more motivating than wanting to stay that way.</p>
]]></content:encoded>
      <category>Product</category>
      <category>Culture</category>
    </item>
    <item>
      <title>Enter the project manager</title>
      <link>https://build.golfnext.com/en/posts/enter-the-project-manager/</link>
      <pubDate>Tue, 06 Oct 2026 00:00:00 +0000</pubDate><dc:creator>John Allan Nielsen</dc:creator>
      <guid>https://build.golfnext.com/en/posts/enter-the-project-manager/</guid>
      <description>Somebody reviewed one of our pull requests properly, line by line, and approved it — while the author was still pushing to the branch. The approval landed on a version that no longer existed. That is a symptom; the disease is that a code review here could be requested four different correct ways and therefore reached nobody. On queues that aren&#39;t queues, what coding agents did to our capacity to review, and why the fix begins by deleting one of the places we ask. Enter the project manager.</description>
      <content:encoded><![CDATA[<p>My degree is in management accounting. Not computer science.</p>
<p>It&rsquo;s the inward-facing kind: financial accounting produces the numbers the outside world is shown, management accounting produces the numbers the people running the thing need in order to decide. Efficiency, planning, budgeting, management — four words that look like four subjects and are really one question asked four ways. Where does a scarce resource go, and how would you know if it went somewhere it shouldn&rsquo;t have.</p>
<p>Point that at software delivery and the word that changes shape first is efficiency. It stops meaning working faster and starts meaning not spending the same scarce resource twice. And the resource isn&rsquo;t money, and it isn&rsquo;t developer hours. It&rsquo;s attention — the sustained kind needed to read somebody else&rsquo;s code properly.</p>
<p>My title is Solution Architect and Technical Project Manager. People ask about the first half, and it&rsquo;s the easy one: I design the architecture for the new services we build, and redesign the parts of what we already run that need it. That&rsquo;s the work with drawings attached.</p>
<p>Nobody has ever asked me anything about the second half, and it&rsquo;s the half that decides whether the first one ever ships. An architecture nobody can find the task for, get a review on, or hear about when it&rsquo;s ready is a drawing. So: the roadmap, task management, GitHub, and how people find out that something has happened.</p>
<p>I&rsquo;ll give the verdict first, because a post that withholds it to sound balanced is wasting your time. The way we request and get code reviews is a mess — no single place answers the question &ldquo;what is waiting for me&rdquo;, and there is no mechanism anywhere in it except people remembering. At one point we had over a hundred pull requests open and marked ready for review. I helped build that. It does not work.</p>
<p>And an accountant would give that pile a less forgiving name than backlog. A hundred finished-but-unmerged changes is <strong>inventory</strong>: work already paid for in full, from which we have realised nothing, sitting in a warehouse and quietly depreciating as the codebase moves on underneath it. It&rsquo;s the most expensive thing in the department, and none of it is on anybody&rsquo;s books.</p>
<h2 id="getting-a-review-sometimes-meant-chasing-a-colleague">Getting a review sometimes meant chasing a colleague<a class="gn-heading-anchor" href="#getting-a-review-sometimes-meant-chasing-a-colleague" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Not always, and not everybody. But often enough to notice, and often enough that it became a part of the job I hadn&rsquo;t expected to be doing.</p>
<p>A pull request is open. It&rsquo;s small, it&rsquo;s ready, and it&rsquo;s been sitting. Everything is where it should be: the branch is pushed, the description says what changed and why, the reviewer is on it. Nothing happens, so I go and ask. Sometimes I ask again.</p>
<p>Some of ours have been sitting for months. Not blocked, not disagreed with, not waiting on a decision. Just open, since spring.</p>
<p>I want to be exact about the failure, because the obvious explanation is wrong. Nobody is refusing. Nobody thinks the change is bad. There is no disagreement anywhere in this, which is what makes it so maddening — it&rsquo;s pure friction, producing no information and no decision, only delay.</p>
<h2 id="we-made-writing-code-cheap">We made writing code cheap<a class="gn-heading-anchor" href="#we-made-writing-code-cheap" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>We use coding agents heavily, and this blog has spent several posts being enthusiastic about them. Here&rsquo;s the invoice.</p>
<p>An agent lets one person keep several things in flight at once. That&rsquo;s real, and it&rsquo;s most of the productivity gain. But work in flight isn&rsquo;t free. Each thread occupies a slot in your head, and the slots don&rsquo;t multiply just because the typing got faster.</p>
<p>So the throughput of <em>producing</em> changes went up and the capacity for <em>reviewing</em> them didn&rsquo;t move at all. Reviewing is still one human, reading, at human speed, and it needs exactly the kind of sustained attention that parallel work is designed to break up.</p>
<p>Two things follow, and they compound. More pull requests arrive. And the people who should read them are each holding three threads of their own, which is precisely the state in which somebody else&rsquo;s branch is easiest to postpone.</p>
<p>I&rsquo;ve <a href="/en/posts/road-to-glory/">written here before</a> that agents made rewrites cheap to produce and no cheaper to review. This is the same sentence pointed at ordinary work. The bottleneck didn&rsquo;t disappear when we got faster. It moved to review, and we didn&rsquo;t move with it.</p>
<h2 id="we-never-chose">We never chose<a class="gn-heading-anchor" href="#we-never-chose" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>The second cause has nothing to do with AI and everything to do with us.</p>
<p>We have a task system. We have GitHub. We have a chat channel specifically for posting pull requests that want reviewing. All three are correct. All three are in daily use. And a review can legitimately be requested in any of them — as a GitHub review request, as a comment on the task, as a post in the channel, or by turning round and saying it out loud.</p>
<p>Four routes for one request. Each defensible on its own; the combination indefensible.</p>
<p>This is worse than plain chaos, because from every individual angle it looks fine. Each request went somewhere reasonable. Each system is being used as intended. Nobody is doing anything wrong. But no single place holds the answer to &ldquo;what is waiting for me right now&rdquo;, so nobody has a queue. They have four partial ones, and assembling the real queue costs more effort than reading the pull request would have.</p>
<p>That&rsquo;s the whole failure, and it isn&rsquo;t a tooling failure. Every one of those systems works. We are drowning in not having chosen between them.</p>
<h2 id="the-doorbell">The doorbell<a class="gn-heading-anchor" href="#the-doorbell" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Underneath the routing problem is a smaller, meaner one.</p>
<p><strong>A comment on a task notifies nobody.</strong> People write a careful comment, on the correct task, with the right detail, and then wait. Nothing happens, because nobody was told. The comment is sitting exactly where it belongs and exactly where nobody is looking.</p>
<p>So my colleagues told me, reasonably: if you need me to read something, tag me. Fair. A mention is what turns a message into a notification — not the message, not the channel, the mention. There&rsquo;s a trap inside that, too: nobody is notified about their own message, so if you post on somebody&rsquo;s behalf under your own name, the one person guaranteed not to hear it is you.</p>
<p>I started tagging. It helped. It did not fix it.</p>
<p>A notification is a doorbell, and a doorbell only works if somebody is home. Tag a person who is three threads deep in their own work and you have successfully delivered an interruption they will consciously defer — and a deferred interruption isn&rsquo;t stored anywhere. It&rsquo;s gone the moment the next thing arrives.</p>
<p>It would be easy to write this as a complaint about colleagues who forget. That isn&rsquo;t what&rsquo;s happening and it isn&rsquo;t fair. <strong>If a process depends on people remembering, the process has already failed</strong> — the remembering is load-bearing, and human memory is the wrong material for that. My colleagues are not the defect. A system whose only mechanism is &ldquo;tag them and hope&rdquo; is the defect, and I built that system.</p>
<h2 id="ready-for-review-doesnt-mean-anything">&ldquo;Ready for review&rdquo; doesn&rsquo;t mean anything<a class="gn-heading-anchor" href="#ready-for-review-doesnt-mean-anything" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Go back to that number. Not a hundred pull requests open — a hundred marked <em>ready</em>, which is a hundred assertions that a change was finished and waiting on somebody.</p>
<p>Then this. Somebody picked one up, reviewed it properly, line by line, and approved it. The author&rsquo;s reaction was that the task wasn&rsquo;t done: he was still working on it, and had been pushing to the branch while it was being read.</p>
<p>So the approval landed on the previous version. The reviewer spent their attention on code that no longer existed by the time they finished — and unless the repository dismisses stale approvals when new commits arrive, that green tick is now sitting on code nobody has read. Worse than an unreviewed pull request, which at least looks unreviewed.</p>
<p>Nobody broke a rule. That&rsquo;s the point: there wasn&rsquo;t one. &ldquo;Ready for review&rdquo; was a label anyone could apply at any moment, meaning anything from &ldquo;this is finished, please read it&rdquo; to &ldquo;this branch exists&rdquo;. A signal that can mean two things means nothing, and a hundred of them mean nothing a hundred times over.</p>
<p>Which also explains the months. A pull request open since spring isn&rsquo;t one somebody keeps deciding to postpone; nobody is deciding anything about it. It stopped being a request the day it became indistinguishable from the other ninety-nine — after that it&rsquo;s a row in a list, and a branch quietly ageing against a codebase that has moved on underneath it.</p>
<h2 id="what-i-got-right-and-what-it-didnt-touch">What I got right, and what it didn&rsquo;t touch<a class="gn-heading-anchor" href="#what-i-got-right-and-what-it-didnt-touch" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Everything I introduced here comes out of one sentence: <strong>each fact appears in exactly one artifact.</strong></p>
<p>What is needed and why lives in the task. What changed and the reasoning lives in the pull request. That a review is wanted lives in the request for it. Why a non-obvious line is the way it is lives in a code comment.</p>
<p>The point isn&rsquo;t tidiness, it&rsquo;s cost. The same fact in four places costs the writer, who retypes it, and every reader afterwards, who now has four sources that agreed on the day they were written and have been drifting since. Restating the ticket in the pull request, and the pull request in the chat message, is the single biggest source of bloat in what we produce — and bloat isn&rsquo;t a style problem. It&rsquo;s why nobody reads any of it.</p>
<p>Where that has worked is documents. Task descriptions cap at forty lines, structured as Problem, Expected, Acceptance criteria. Pull request descriptions cap at 250 words, and a trivial change gets five lines and no headings: no test counts, no &ldquo;CI green&rdquo;, no file inventories, because CI reports its own status and the diff lists its own files. Writing those out again just produces a staler copy of what&rsquo;s already on screen, and pushes the one thing only the author knows — the <em>why</em> — further down the page.</p>
<p>My favourite, and the one people argue with: <strong>when your understanding of a task changes, rewrite the description.</strong> Don&rsquo;t append. No &ldquo;revised&rdquo;, no &ldquo;supersedes&rdquo;, no dated changelog inside the description. It feels like destroying evidence, and it isn&rsquo;t — the tool keeps history, the same way git does. The diary version leaves the reader working out which paragraph is still true, which is the exact job the description existed to do for them. A task says what is true now.</p>
<p>That rule works, because it governs <em>where a fact is written</em>, and writing is a deliberate act a convention can catch. It has no grip whatsoever on where a request gets <em>answered</em>. A convention can tell you where to put something. It cannot make anybody pick it up.</p>
<p>Which is the honest shape of my first months here. I wrote rules for the artifacts, because artifacts are the part a newcomer can change alone, and then assumed the human half would follow the documents into order. It didn&rsquo;t. The documents are genuinely better. The flow around them is the same mess it was, and now it&rsquo;s a mess with well-formatted contents.</p>
<h2 id="choosing-is-subtraction">Choosing is subtraction<a class="gn-heading-anchor" href="#choosing-is-subtraction" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Choosing sounds like adding a rule. It isn&rsquo;t. The whole move is subtraction.</p>
<p><strong>One place to ask, and it&rsquo;s GitHub.</strong> A review is requested on the pull request, using GitHub&rsquo;s own review request. That&rsquo;s the entire routing decision.</p>
<p><strong>No code review channel.</strong> This is the part that costs something, because the channel works — people post there, it&rsquo;s a reasonable thing to do, and I&rsquo;m the one who was asking colleagues to use it. It goes anyway. A second correct place to ask is what created the problem, and keeping it because it&rsquo;s harmless on its own is exactly how we got to four routes. You can&rsquo;t half-choose.</p>
<p>GitHub wins for reasons that have nothing to do with it being better software. The request lives next to the thing being requested, so there&rsquo;s no gap between the notification and the work. Its state survives everything — a restart, a holiday, a forgotten mention. And it doesn&rsquo;t depend on anybody remembering, which is the property every other option lacked.</p>
<p><strong>The task system keeps its job, which is status, not requests.</strong> The task moves through its statuses so the board is an honest picture of where development actually is. It isn&rsquo;t where you ask for a review; it&rsquo;s where you look to see what&rsquo;s happening.</p>
<p><strong>Soon the two stop being updated by hand.</strong> When a pull request is merged, the task should move from code review to ready for release on its own. That&rsquo;s the one-fact rule pointed at status instead of prose: whether something is merged is a fact GitHub already owns, and every manual status update is a second copy of it, kept in step by a human who is busy. Copies drift. A projection can&rsquo;t.</p>
<p><strong>Then the habit: your pull request inbox.</strong> GitHub keeps one at <a href="https://github.com/pulls/inbox">github.com/pulls/inbox</a> — everything waiting on you, in one list, with a number on it. Check it often, and specifically <strong>check it before you start a new task</strong>. That placement does most of the work: it puts reviewing at the moment you&rsquo;re between things rather than asking you to interrupt yourself, and it means the queue gets shorter every time somebody starts something new instead of longer.</p>
<p><strong>And a pull request stays a draft until its author believes it&rsquo;s ready to release.</strong> Not ready to read — ready to release, with the change already on staging and through acceptance testing. Draft is the default, and a draft costs nothing to open.</p>
<p>That inverts what we had. &ldquo;Ready for review&rdquo; used to be the default state, which is why it carried no information and why somebody could approve a version the author was still working on. Make ready a deliberate act and it starts meaning something again — and &ldquo;am I still pushing to this?&rdquo; becomes the author&rsquo;s question to answer up front, instead of the reviewer&rsquo;s to discover afterwards.</p>
<p><strong>One task, one pull request.</strong> If a task needs more than one, it isn&rsquo;t one task: break it into subtasks and give each of them a pull request. I expect more argument about this than anything else here, and it&rsquo;s the one I&rsquo;d defend hardest, because it isn&rsquo;t really a rule about linking. It&rsquo;s a decomposition forcing function. The moment you find yourself opening a second pull request against the same task, the task has told you it was too big — and it tells you at the point where splitting it is still cheap, rather than four months later when somebody has to review the result.</p>
<p>It&rsquo;s also what makes the automation possible. &ldquo;When a pull request is merged, move the task to ready for release&rdquo; only means something if a task has one pull request. With three it&rsquo;s ambiguous, somebody has to arbitrate, and we&rsquo;re back to a human keeping two systems in step by hand.</p>
<p>For genuinely large work, the parent task gets a feature branch and the subtask pull requests target that branch instead of master. The parent merges once, when the whole thing is done. That gives one place to watch the state of a big piece of work without anybody pretending it was ever a single change.</p>
<p>I should be straight about the tension in that last one. I&rsquo;ve <a href="/en/posts/in-the-jungle/">argued elsewhere</a> for collapsing to a single long-lived branch, and a feature branch is a long-lived branch. The distinction I&rsquo;d hold is duration and ownership: this one exists for the life of one parent task and is deleted with it, where the development and staging and production branches we&rsquo;re retiring exist forever and quietly accumulate divergence. That&rsquo;s a scoped exception rather than a reversal — but it is an exception, and if I find these branches still alive months from now, I was wrong about it.</p>
<p>The weak point is worth naming. Two of these are mechanisms and one is a habit. GitHub as the single route is enforceable, draft-by-default nearly so. &ldquo;Check your inbox before you start something&rdquo; is a habit, in a post largely about how the last thing I built on a habit didn&rsquo;t hold.</p>
<p>What&rsquo;s different, I think, is that the inbox is an actual queue rather than a request to remember. It has a number, it&rsquo;s the same number tomorrow, and it doesn&rsquo;t quietly empty itself when your attention moves. That&rsquo;s a better foundation than a chat channel you have to think to open. But it&rsquo;s still a habit, and if it fails it will fail the same way.</p>
<h2 id="what-a-piece-of-work-actually-looks-like">What a piece of work actually looks like<a class="gn-heading-anchor" href="#what-a-piece-of-work-actually-looks-like" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>A large task arrives, or a small project. Then, roughly in this order:</p>
<p>I design a solution for it. I break that into tasks and subtasks. I check that every application the thing touches can talk to the others in the way the design assumes — which is where most of the unpleasant surprises live, because two systems agreeing in a diagram is not the same as two systems agreeing about a payload. I check it&rsquo;s secure, at the boundaries where it&rsquo;s easy to be lazy. And I keep asking whether the result is still worth something to the people who asked for it, because it is entirely possible to design something correct, well-decomposed, secure, and of no value to anybody.</p>
<p>That&rsquo;s both halves of the title in one piece of work, which is the honest reason they&rsquo;re on one business card. The design is the architect half. Everything after the first sentence is the project manager half, and it&rsquo;s longer.</p>
<p>I plan most of it with Claude. Not to write the plan — to interrogate it. I&rsquo;ll put an approach in and ask what&rsquo;s wrong with it, ask for the alternatives I haven&rsquo;t thought of, ask what each one costs. It&rsquo;s very good at that, and it&rsquo;s fast enough that evaluating three designs is no longer a thing you skip because you&rsquo;ve already sunk a morning into the first one.</p>
<p>It has one failure mode you have to actively manage: it is agreeable. Push a mediocre idea confidently and you will often get help making it better rather than a flat &ldquo;this is the wrong shape&rdquo;. So when a decision matters, or when I&rsquo;m genuinely unsure, I take it to a second and sometimes a third colleague.</p>
<p>That isn&rsquo;t a formality. A model will help me think, and it will not tell me that I&rsquo;m the third person this year to propose this, or that the last attempt failed for a reason nobody wrote down. Institutional memory doesn&rsquo;t live in a context window. It lives in the people who were here.</p>
<h2 id="slicing-it-three-ways">Slicing it three ways<a class="gn-heading-anchor" href="#slicing-it-three-ways" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>The other initiative is duller to describe and probably matters more: projects get broken into sub-projects, each small enough to actually finish, each with the business involved during it rather than at the end.</p>
<p>The reason isn&rsquo;t planning neatness. A chunk that takes four months produces exactly one moment where anyone outside the work can react to it, and that moment arrives after every expensive decision has been made. You don&rsquo;t get feedback, you get a verdict. It produces something else, too, which this post has already spent two thousand words on: a pull request nobody can review. The mountain isn&rsquo;t only a habit problem. Some of those branches are simply too big to read, and they were too big on the day they were opened.</p>
<p>There&rsquo;s a second reason, and it&rsquo;s about people rather than feedback loops.</p>
<p>The plan is long. One language behind everything, the same shape in every repository, two admin portals becoming one — that&rsquo;s measured in seasons, and I&rsquo;ve <a href="/en/posts/road-to-glory/">said as much in public</a>. Stated whole, a plan like that motivates nobody. It&rsquo;s a wall. And we have a plethora of them queued up, each genuinely worth doing, each months deep.</p>
<p>Breaking them down doesn&rsquo;t make them shorter. The months are still the months. What changes is how often you get evidence the thing is working, and that turns out to be what decides whether people keep going. A dozen multi-month projects is a mood. The same dozen, with the first slice of each finishing inside a fortnight, is something you can hold in your head and keep walking towards, because you can watch it move.</p>
<p>That&rsquo;s underrated as a reason to plan properly. Not so the plan is correct — it won&rsquo;t be — but so the distance between &ldquo;we agreed to do this&rdquo; and &ldquo;something happened&rdquo; is short enough that people stay pointed at it. That&rsquo;s forecasting rather than planning: you don&rsquo;t get better by predicting harder, you get better by shortening the interval before reality tells you something. The caution is that a well-broken-down plan is easy to keep following after it stops being right. The slices are small so you can change your mind between them, not so you can stop having to.</p>
<p>This is also the least visible part of the job. I help our product manager build and maintain the roadmap, and most of that work isn&rsquo;t deciding what goes on it — ideas are not the scarce resource here — but deciding what a line on it is allowed to mean. An entry that can&rsquo;t finish inside a quarter isn&rsquo;t a plan, it&rsquo;s a wish with a date attached. Breaking those entries down until each one is something that can actually complete, and then staying on them until they do, is most of what &ldquo;making sure we reach our goals&rdquo; turns out to consist of. It&rsquo;s unglamorous, and it&rsquo;s the difference between a roadmap that describes the year and one that predicts it.</p>
<p>What makes this hard is that a slice has to satisfy three audiences at once.</p>
<p>It has to be <strong>beneficial for users</strong> — deliver something somebody outside the team can use, or at minimum see. It has to be <strong>manageable for developers</strong> — small enough to hold in your head, review in one sitting, and abandon without grief. And it has to be <strong>sensible for the business</strong> — a step somebody can say yes or no to, in terms that aren&rsquo;t about code.</p>
<p>Miss any one and the slicing does nothing. The failure I&rsquo;ve seen most is cutting along developer-shaped lines, because those are easiest to plan: all the data models, then all the endpoints, then all the screens. Perfectly sensible engineering order, and it produces months of work nobody outside can evaluate, because none of it is a thing yet. You&rsquo;ve divided the work without dividing the risk.</p>
<p>Two shapes this takes for us.</p>
<p><strong>Release it to ourselves first.</strong> A new analytics tool goes to GolfNext employees only, for a week. Then it expands, and what it expands into is shaped by what everybody said. This blog has <a href="/en/posts/four-products-68-repos/">admitted before</a> that we have no gradual rollout mechanism and that our first real ring is geography — a few nearby clubs, and a recovery plan involving twenty minutes in a car. Staff-first is a ring that sits before that one, costs nothing, and could exist this week. It isn&rsquo;t a canary deployment. It&rsquo;s the cheapest possible way to find out something obvious is wrong before a club does.</p>
<p><strong>Refactor a module at a time.</strong> Not the whole system, and not an arbitrary slice either: one cut where all three constraints hold at once. The users get something, the developers get a piece they can finish, the business gets a step worth funding. That&rsquo;s the same discipline as the seams-before-rewrites rule I&rsquo;ve <a href="/en/posts/road-to-glory/">argued for elsewhere</a>. What&rsquo;s new is that finding the cut <em>is</em> the work. It isn&rsquo;t a planning formality you complete before the real job starts.</p>
<p>I&rsquo;d rather not oversell it. Slicing badly is worse than not slicing, because a bad cut ships half a feature and teaches everyone that incremental delivery means unfinished things. The skill is in where the knife goes, and I don&rsquo;t think that&rsquo;s something you can write down as a rule. It&rsquo;s the part of this job I&rsquo;d defend as genuinely requiring judgement — which is an uncomfortable thing to admit in a post otherwise arguing that the answer is fewer choices and more mechanisms.</p>
<h2 id="the-question-underneath-all-of-it">The question underneath all of it<a class="gn-heading-anchor" href="#the-question-underneath-all-of-it" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>We&rsquo;re planning to change the architecture and the stack: one language behind everything, one shape in every repository. I&rsquo;ve <a href="/en/posts/road-to-glory/">written about that elsewhere</a>. What matters here is the reaction. My colleagues agree it&rsquo;s a good idea. Not grudgingly. They think it&rsquo;s right.</p>
<p>And almost nothing moved in that direction before <a href="/en/author/jb/">Johann</a> and I turned up.</p>
<p>That bothered me for a while, and the lazy explanations are all wrong. It wasn&rsquo;t permission — this company says yes to an idea in a day, and I&rsquo;ve had ideas approved here faster than I could have booked a meeting at my last job. It wasn&rsquo;t ability. And it certainly wasn&rsquo;t that nobody had thought of it: these are people who know this estate far better than I do, and I&rsquo;d bet most of what&rsquo;s in our plan had been said out loud, in some form, years before I arrived.</p>
<p>So why did nothing happen?</p>
<p><strong>Because agreement isn&rsquo;t a mechanism.</strong> A thing everybody agrees about and nobody owns has no way to start. There&rsquo;s no disagreement to resolve, so no meeting gets called. There&rsquo;s no customer waiting, so it never wins a Tuesday against something that has one. It doesn&rsquo;t help that the work looked like an undifferentiated multi-month blob with no early payoff, which is exactly the shape nobody volunteers for — and exactly what breaking it down is for. It simply stays true and undone, and being obviously correct does nothing to move it. You can watch the same effect in a hundred pull requests nobody disagrees with.</p>
<p><strong>Because for most of this company&rsquo;s history, nobody&rsquo;s job was bigger than one project.</strong> A lot of our software was built by external partners, and they were good at it. But you engage a partner to deliver a thing. Nobody engages a partner to fix what sits <em>between</em> the things. Cross-cutting work has no client, so under that model it doesn&rsquo;t get built, and thirteen years of it accumulates in the gaps.</p>
<p><strong>Because the people who could have done it are the people everything else queues behind.</strong> That&rsquo;s the bus factor from another side: whoever knows a system best is who every request for it waits on, so they have the least slack precisely because they have the most knowledge. You don&rsquo;t restructure an estate from inside that.</p>
<p><strong>And because the year has a shape.</strong> Golf is seasonal. The clubs are busy in summer, so we&rsquo;re busy in summer, and the slack is in winter — which makes the window for structural work narrow, annual, and easy to spend on something louder.</p>
<p>Which brings me to the uncomfortable part, and to the reason for the second half of my title.</p>
<p>What Johann and I brought was not better ideas. It was an empty queue and permission to ask why. A newcomer carries no legacy backlog for their first months, is expected to ask stupid questions, and hasn&rsquo;t yet internalised any of the local &ldquo;that&rsquo;s just how it is&rdquo;. That&rsquo;s a real resource with a short shelf life; every week here spends a little of it.</p>
<p>So the project manager half isn&rsquo;t there to impose process on people who were doing fine without it. It&rsquo;s there to own the things everyone agrees with and nobody has. Somebody has to convert agreement into a task with a name on it, and that turns out to be a job rather than an afterthought.</p>
<p>Is any of it more enjoyable? The documents are. A task you can read in a minute, a pull request that tells you why instead of what, and a great deal less retyping of things already written down somewhere else. The review flow is not more enjoyable. It&rsquo;s diagnosed, which is not the same thing, and every fix above is at most a fortnight old.</p>
<p>Which is why the queue is an uncomfortable thing to write about. It&rsquo;s the same failure as the one I&rsquo;ve just spent four paragraphs explaining, in my own area, on my watch.</p>
<p>Everybody agrees those hundred pull requests should be reviewed.</p>
]]></content:encoded>
      <category>Management</category>
      <category>Engineering</category>
    </item>
    <item>
      <title>One day in Ry</title>
      <link>https://build.golfnext.com/en/posts/one-day-in-ry/</link>
      <pubDate>Fri, 02 Oct 2026 09:00:00 +0000</pubDate><dc:creator>Johann Böhler</dc:creator>
      <guid>https://build.golfnext.com/en/posts/one-day-in-ry/</guid>
      <description>Eight weeks of real moments folded into a single day at the office in Ry: an idea said out loud over coffee, a phone call to a club that hadn&#39;t noticed its kiosk was down, a dashboard the founder can&#39;t walk past, players asking for our devices by name, and the evening I sent 94 kiosks into a restart loop and patched them back one by one.</description>
      <content:encoded><![CDATA[<p>It&rsquo;s a little after nine in the evening, and I&rsquo;m connected to a kiosk at a golf club, patching it by hand. When it&rsquo;s done I&rsquo;ll connect to the next one. There are quite a few of them left.</p>
<p>We&rsquo;ll get back to that. I&rsquo;ve been at GolfNext for eight weeks, and every time I tried to write about them I ended up with a list. So instead I&rsquo;ve folded eight weeks of real moments into a single day at the office in Ry, from the first coffee to the last kiosk. Nothing below happened on one day. All of it happened.</p>
<h2 id="0740-what-colour-is-it-today">07:40. What colour is it today?<a class="gn-heading-anchor" href="#0740-what-colour-is-it-today" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Before I leave for work there&rsquo;s one decision to make, and it&rsquo;s mostly made for me. We have company t-shirts in different colours, and on three days a week we all wear the same one: blue Monday, green Wednesday, black Friday. This day is folded together from eight weeks, so I can&rsquo;t tell you which shirt I&rsquo;m wearing. Possibly all three. Either way, at GolfNext Black Friday comes every week.</p>
<h2 id="0750-coffee-and-an-idea-said-out-loud">07:50. Coffee, and an idea said out loud<a class="gn-heading-anchor" href="#0750-coffee-and-an-idea-said-out-loud" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>The coffee machine is where most things start here. Somebody says &ldquo;what if we just&hellip;&rdquo; and by lunch there&rsquo;s a branch. Sometimes an idea from the morning is live within 24 hours. Nobody has to approve you trying something. Code review and the release process still apply, but the distance between &ldquo;what if&rdquo; and &ldquo;it&rsquo;s running&rdquo; is short, and everyone knows it. <a href="/en/author/jn/">John</a> wrote a whole post about this, <a href="/en/posts/culture-of-enthusiasm/">the culture of enthusiasm</a>, and he&rsquo;s right.</p>
<p>My favourite example has a silly name, which I&rsquo;m allowed to say because I made it up. <em>All you can Ball.</em> At a driving range you normally buy a bucket of balls. With All you can Ball you buy a time window instead, and one code dispenses again and again until your time runs out. Each portion is sized by the time you have left, so nobody empties the machine in minute one and then stands around for fifty-nine minutes looking pleased with themselves.</p>
<p>Then, over another coffee, the next thought. Why have bucket sizes at all? Long-press the card terminal and balls flow for as long as you hold it, like a fuel nozzle. I call it the beer-glass effect: people stop when it looks full. Peter, who founded the company in 2012, loved it. It&rsquo;s still an idea. Ideas are cheap here, which is exactly why so many of them turn into something.</p>
<h2 id="0930-someone-walks-over">09:30. Someone walks over<a class="gn-heading-anchor" href="#0930-someone-walks-over" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>This is the part I didn&rsquo;t expect to matter as much as it does.</p>
<p>Hjalte walks over. A PDF of ball codes that clubs print out is spilling onto two pages. Not a crisis. The kind of thing that sits in a backlog for a month because it&rsquo;s never quite the most important thing.</p>
<p>John fixed it within the hour.</p>
<p>Another evening, members at a club get charged guest prices. I sit down with one of our external developers, we find it together, and it&rsquo;s fixed within the hour. Those external colleagues built the kiosks, the booking flows and the payment integrations, and kept them running for more than a decade before any of us showed up. Having them and us in the same conversation is the best of both.</p>
<h2 id="1100-the-phone-call">11:00. The phone call<a class="gn-heading-anchor" href="#1100-the-phone-call" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Late morning I notice a kiosk at one club has gone offline. I mention it. Hjalte doesn&rsquo;t open a ticket. He picks up the phone and calls the club.</p>
<p>They hadn&rsquo;t noticed yet. They were glad we called.</p>
<p>From then on, that&rsquo;s the rule: when we see something wrong, we call. It&rsquo;s a tiny thing, and it works because the person who sees the problem and the person who knows the club are in the same building.</p>
<h2 id="1200-down-the-desk">12:00. Down the desk<a class="gn-heading-anchor" href="#1200-down-the-desk" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Max joined at the end of August to look after the mobile app players use. On his first day I told him the app was his: do whatever you think is best, I won&rsquo;t second-guess you. He started with the unglamorous part, taking the app&rsquo;s TypeScript errors from 134 to zero and its lint errors from 264 to zero, and getting automated tests running on real phones. None of that is visible to a golfer. All of it makes the next feature safer to ship.</p>
<h2 id="1300-the-wall-boards">13:00. The wall boards<a class="gn-heading-anchor" href="#1300-the-wall-boards" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>After lunch everyone ends up in front of the TVs.</p>
<p>They arrived a while ago, delivered alongside somebody else&rsquo;s pallet of cat food, which went back. One evening, unasked, I built a dashboard for them with Claude. Latest releases. What&rsquo;s live where, because support had asked. Open pull requests. A heartbeat from every device out in the field. Balls dispensed per club today versus the same day last week. And a random programming meme, because a dashboard without a joke on it is just a spreadsheet on a wall.</p>
<p>The next day Peter kept stopping in front of it. Mostly at the ball numbers, watching clubs overtake each other as the day went on. A day later the CEO sent over sales slides to add. It&rsquo;s become the screen we stand around with coffee, which is more than I can say for any dashboard I&rsquo;ve built before.</p>
<h2 id="1430-the-twins">14:30. The twins<a class="gn-heading-anchor" href="#1430-the-twins" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>In the afternoon John and I both open a pull request for the same feature: choosing which version of the software a kiosk runs. Neither of us had told the other. Same idea, same day, two implementations.</p>
<p>We merged the two. It isn&rsquo;t the first time we&rsquo;ve done something like that. We call each other the twins now, because we keep arriving at the same place from different directions. If two people who didn&rsquo;t talk to each other end up in the same spot, it&rsquo;s probably a good spot.</p>
<p>That feature belongs to the biggest thing I&rsquo;ve built here, the fleet service. It&rsquo;s one system that knows every physical device we have out there, kiosk screens, card terminals, ball dispensers, and manages them from one place. A device joins once with a 12-digit code typed in on site, gets its own identity, and from then on takes its settings and its version from the service instead of from someone fiddling with it by hand. We switched it on overnight in September, with clubs closed. By morning every club kiosk had joined. The only holdout was our own demo unit in Sweden, which had been set up twice and complained that a device with its ID already existed. Of course it was the demo unit.</p>
<p>Hjalte has a better name for the enrolment part. He calls it the 1-Click installer. Nobody has to connect to a device and set up its environment by hand any more. A 12-digit code, and the device is ours. That&rsquo;s how we&rsquo;ll ship from now on. The next step, and we&rsquo;re close, is a small web page where staff at a club set up a new device themselves, which I suspect will be the fun kind of setup. And replacing a broken device won&rsquo;t take several phone calls. It&rsquo;ll take a parcel and a link.</p>
<h2 id="1500-someone-elses-clock">15:00. Someone else&rsquo;s clock<a class="gn-heading-anchor" href="#1500-someone-elses-clock" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Mid-afternoon, a message from a club in Portugal: their ball dispenser opens about an hour late, every single morning. Nobody had touched the schedule. It turned out the dispenser was mixing up its own time zone with Denmark&rsquo;s, and so was every dispenser outside Denmark. Money had the same habit. Prices assumed Danish kroner with two decimals, which had to change before a club in the UK could take pounds, and before our kiosk in Iceland could go live at all, because the Icelandic króna has no decimals. Now time and currency come from the club, not from wherever our office happens to be.</p>
<h2 id="1600-the-news">16:00. The news<a class="gn-heading-anchor" href="#1600-the-news" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Late in the afternoon one of our longest-serving colleagues tells the team something they&rsquo;ve been hearing. Players have started going to their clubs and asking whether they can get GolfNext&rsquo;s devices. Because they&rsquo;re so easy to use. Because they&rsquo;re convenient.</p>
<p>For me that landed harder than any metric on the wall. So much of our work only gets noticed when something goes wrong. Meanwhile, somewhere, a golfer is standing at a counter asking for the thing we make.</p>
<h2 id="1800-the-day-that-ran-long">18:00. The day that ran long<a class="gn-heading-anchor" href="#1800-the-day-that-ran-long" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Which brings us back to the evening. This part is from my third week.</p>
<p>I&rsquo;d been chasing a bug where kiosks sometimes froze. I wanted more detail, so I made the kiosk pause before it exited, just long enough to write a crash log. What I hadn&rsquo;t accounted for is how kiosks update: the updater closes the app, swaps the files, and only waits so long. My pause made the app too slow to close. Updates kept getting cut off, and around 94 kiosks started restarting in a loop. Updates only run when a kiosk is idle on its home screen, so nobody was thrown out halfway through buying a round. It was still 94 kiosks going round in circles. And the crash log didn&rsquo;t even help me find the freeze.</p>
<p>The awkward bit was that I couldn&rsquo;t just ship a fix. The update logic that runs is the one already sitting on the kiosk, the same one that was broken.</p>
<p>It took about two hours to stop the bleeding by freezing the update the kiosks were pulling. Then came the rest of the evening, the scene at the top of this post: connecting to kiosks one at a time and patching them by hand so the next release would stick.</p>
<p>It did.</p>
<h2 id="0800-the-next-morning">08:00, the next morning<a class="gn-heading-anchor" href="#0800-the-next-morning" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>I came in braced for jokes. There weren&rsquo;t any. Our product manager wasn&rsquo;t worried. People were glad someone had put the hours in.</p>
<p>There&rsquo;s a German saying, <em>wo gehobelt wird, fallen Späne</em>. Where wood gets planed, shavings fall. We&rsquo;ve done a lot of planing in eight weeks, roughly 650 merged pull requests between John, Max and me, and that night was one of the shavings. It also left something useful behind. Kiosks now give up after three failed update attempts and keep the version they have. And the night is the reason the fleet service rolls things out the way it does: a new version goes to a few kiosks first, and once it works, it goes to the rest. If something&rsquo;s wrong, a kiosk goes back to its previous version from a setting. That&rsquo;s the part of the evening I&rsquo;m proudest of.</p>
<h2 id="why-the-building-matters">Why the building matters<a class="gn-heading-anchor" href="#why-the-building-matters" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>On the last Friday of September, John and I walked out to the car park together and agreed it had been a big week. &ldquo;It&rsquo;s quite noticeable what we bring to the company.&rdquo;</p>
<p>I&rsquo;ve thought about why, and I keep coming back to the building, or really to how short the distances in it are. The PDF that gets fixed within the hour because someone walked over. The phone call that happens because the person who spotted the problem is sitting next to the person who knows the club. The feature built twice because two people think alike, and merged because they sit together. The good news that reaches the whole team at once, in the same room.</p>
<p>It&rsquo;s also what Friday mornings are for. For half an hour we all sit round one table, in black shirts, with <em>rundstykker</em>, Danish breakfast rolls, and talk about our weeks. The distances don&rsquo;t get any shorter than that.</p>
<p>Tomorrow there&rsquo;ll be another coffee, and somebody will say &ldquo;what if we just&hellip;&rdquo; I can&rsquo;t wait to hear what it is.</p>
]]></content:encoded>
      <category>Culture</category>
      <category>Engineering</category>
    </item>
    <item>
      <title>Our agents drank all the coffee</title>
      <link>https://build.golfnext.com/en/posts/agents-drank-the-coffee/</link>
      <pubDate>Sat, 26 Sep 2026 09:00:00 +0000</pubDate><dc:creator>Johann Böhler</dc:creator>
      <guid>https://build.golfnext.com/en/posts/agents-drank-the-coffee/</guid>
      <description>Our coding agents push, wait for CI, fix and push again, all day. When the test suite grew two and a half times in a week, every CI run sat eighteen minutes at the coffee machine. How we got that to about three minutes for the same money, and what was left waiting once the machines stopped being the excuse.</description>
      <content:encoded><![CDATA[<p>There&rsquo;s an xkcd about the best excuse a programmer ever had, and for most of my career it was a documentary. You pushed a commit and walked to the kitchen. You watched the coffee machine grind. You drifted outside to where the smokers stood, because that&rsquo;s where the real decisions got made, and you came back. Still running. The build was the only break anyone was allowed to take.</p>
<figure><img src="/en/posts/agents-drank-the-coffee/xkcd-compiling.png"
			alt="Two developers sword-fighting on office chairs. Their boss asks why they aren&#39;t working; they shout Compiling!"><figcaption>
			<p><a href="https://xkcd.com/303/">xkcd #303, “Compiling”</a> by Randall Munroe, <a href="https://creativecommons.org/licenses/by-nc/2.5/">CC BY-NC 2.5</a></p>
		</figcaption>
</figure>

<p>You know the feeling. You have a fix. It&rsquo;s small, it&rsquo;s ready, all it needs is a green tick. CI says twenty minutes. So you do the sensible thing and start something else, and by the time the run finishes you&rsquo;re deep in that. You look after lunch. It&rsquo;s red: a flaky test, or a conflict with something that merged in the meantime. You fix it, push, wait again, and get back to it tomorrow. A one-hour change has taken two days, and almost none of that time went into the change. All you wanted was to pass the checks and move on.</p>
<p>This year the build got new company at the coffee machine: our coding agents. In September they were waiting eighteen minutes for every run. Here&rsquo;s how that became about three, for the same money, and what we found at the end of the queue once the machines stopped being the excuse: us.</p>
<h2 id="who-waits-for-ci-now">Who waits for CI now<a class="gn-heading-anchor" href="#who-waits-for-ci-now" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Most of our team now works with coding agents, Claude in particular. A typical developer here has four or more agent sessions going at once.</p>
<p>Each of those sessions works in a loop. It writes the change, pushes it, waits for CI (continuous integration: the robot that builds and tests every change), reads what failed, fixes it and pushes again. Only when the checks are green does a person review it, with <a href="https://www.coderabbit.ai">CodeRabbit</a> as an extra pair of eyes, and a person merges it and decides when it reaches production.</p>
<p>That turns CI from something a person checks once or twice an hour into something every agent on the team waits for, all day. On GitHub Actions we went from 48 runs per working day in June to 375 in September. Merged pull requests went from around 140 a month in the spring to 583 in September. GitHub bills by the minute, and the bill went from about $29 in June to $135 for September, with a few days still to go.</p>
<p>Then our main web application got slow, because we had written a lot of tests. In one week in September, while we built a new admin API, the suite grew from about 3,900 tests to about 9,600, most of them tests that boot the whole application and talk to a real database. A typical pipeline run went from under six minutes to eighteen, and a bad one took twenty-seven. Jobs weren&rsquo;t stuck in a queue. The machine had two CPUs, two and a half times as much work, and we had just told it to run eight test processes at once.</p>
<p>A pull request here typically gets two CI runs before it&rsquo;s done, and a quarter get four or more, mostly follow-up pushes rather than failures. At eighteen minutes a run, that&rsquo;s over half an hour per change before a person even looks at it, and with four or more sessions per developer, at least four changes are always at the coffee machine.</p>
<h2 id="not-a-vps">Not a VPS<a class="gn-heading-anchor" href="#not-a-vps" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>A colleague was the first to say it out loud: this is slowing all of us down. Their suggestion was a VPS, a rented virtual server. Rent a big one, install GitHub&rsquo;s runner software (the agent that picks up CI jobs) on it, done. It would have worked.</p>
<p>I still didn&rsquo;t love it. A self-hosted runner is a pet. Somebody patches it, keeps Docker healthy on it, installs the right languages and versions for every repository that uses it, empties the disk when a build fills it, and notices when it&rsquo;s down on a Saturday. I&rsquo;d rather nobody on a team our size spent their week on that. So I looked for something hosted instead, but first I fixed the tests.</p>
<h2 id="fixing-the-tests-then-the-machines">Fixing the tests, then the machines<a class="gn-heading-anchor" href="#fixing-the-tests-then-the-machines" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>The first fix was plain engineering, and it did most of the work. The tests already ran in parallel, but with a lot of waste around them: every test worker built its own copy of the database, the database carefully saved every write to disk as if it mattered, and the same code was compiled again in every process. So the test database now lives in memory, it&rsquo;s built once and copied to each worker, and the suite is split into four jobs that run at the same time. The split is deliberately dumb: count the tests in each file, weigh the slow kinds heavier, and hand the files out heaviest first to whichever job has the least so far. The four jobs don&rsquo;t finish at quite the same time, and timing-based weights are the obvious next step.</p>
<p>Database setup went from 76 seconds to under 30, the tests needed half the compute, and on GitHub&rsquo;s standard machines the pipeline was down to about five minutes. But each of the four jobs now pays for its own setup, so billed minutes per run stayed at about 21, what the slow pipeline had cost. We waited about a quarter as long for the same bill.</p>
<p>The day the test fixes landed, I signed up for a trial at <a href="https://www.blacksmith.sh">Blacksmith</a>, the hosted option with the easiest setup. Blacksmith sells what GitHub sells, a machine that runs your GitHub Actions jobs, except you can pick bigger and faster ones. You don&rsquo;t install anything. You change one line per job:</p>
<pre tabindex="0"><code>runs-on: blacksmith-4vcpu-ubuntu-2404   # was: ubuntu-latest
</code></pre><p>Right after signing up I asked Blacksmith to run our jobs in their EU region. CI should only ever see test data, never a club&rsquo;s or a golfer&rsquo;s. In case someone slips up, the machines sit in the EU anyway. They switched it within a day. There&rsquo;s no deal behind this post: we did the trial and now pay the normal price. If we ever want to leave, it&rsquo;s the same one line back.</p>
<p>With the four test jobs on Blacksmith, the pipeline has run 62 times since. The median is 2.8 minutes, and nine runs in ten finish in under 3.7.</p>
<p>Tuning and splitting took a typical run from eighteen minutes to about five. The machines took it from five to under three, and part of that is simply more cores, because two CPUs became four. I didn&rsquo;t test GitHub&rsquo;s own four-core runner, which exists and costs $0.012 a minute, twice its two-CPU rate, so some of this gain is money any GitHub customer could spend.</p>
<h2 id="what-changed-and-what-didnt">What changed, and what didn&rsquo;t<a class="gn-heading-anchor" href="#what-changed-and-what-didnt" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>I expected the whole loop to collapse. So I measured it, comparing pull requests from the slow week with the five days since the switch.</p>
<p>The part CI controls did collapse. From opening a pull request to its first green run took a median of 17.5 minutes in the slow week. Now it&rsquo;s 3.1. That&rsquo;s the wait every change pays before review, and it&rsquo;s five times shorter.</p>
<p>The part I thought would follow didn&rsquo;t. When a run fails, the time until the next green one is a median of about half an hour, before and after (only a handful of cases each week, so read that loosely). Faster machines couldn&rsquo;t touch it. Fewer than one pull request in ten goes red, and the few that do mostly sit until a person picks them up, sometimes not until the next morning. The agents rarely hand CI something broken. The slow part of our loop is now us.</p>
<h2 id="what-it-costs-and-two-lessons">What it costs, and two lessons<a class="gn-heading-anchor" href="#what-it-costs-and-two-lessons" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>One pipeline run of the main web application used to bill about 21 minutes on GitHub&rsquo;s two-CPU machine, roughly 13 cents. On Blacksmith the same run bills about 16 minutes, most of them on the bigger machines, and comes to 12 or 13 cents depending on how you round. The same money, then, for a run under three minutes instead of five, or eighteen before we touched the tests.</p>
<p>Blacksmith for September so far: $12.68 of usage, $12 of which the free allowance covers. We owe them 68 cents.</p>
<p><strong>Splitting buys time, and you still pay for every minute.</strong> Every job pays for its own setup: checking out the code, installing dependencies, starting the database. GitHub bills each job by the minute, rounded up, so ten one-minute jobs cost ten minutes and feel like one. That&rsquo;s worth it for a pull request somebody is waiting on. For a nightly job nobody watches, keep it in one piece.</p>
<p><strong>Look at ARM early.</strong> At Blacksmith an ARM minute costs over a third less than a standard Intel or AMD (x64) one, and moving CI there doesn&rsquo;t change what you ship. We&rsquo;ve only tried it on one repository so far, and it won&rsquo;t fit everywhere: our Android app has to stay on x64 because Google&rsquo;s Android build tools don&rsquo;t run on Linux ARM.</p>
<h2 id="moving-the-rest">Moving the rest<a class="gn-heading-anchor" href="#moving-the-rest" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>We have nearly fifty active repositories, and I don&rsquo;t want each migration to depend on someone understanding that particular pipeline. So I wrote the process down as a skill for Claude, a runbook it follows step by step when it moves a repository over. It measures before and after, over several runs rather than one lucky one. It checks that caches actually restore; one of ours never once had, and nobody had noticed. And it tries ARM where nothing rules it out.</p>
<p>Five repositories run on Blacksmith so far. One started there. Three, including the main web application, went over with Blacksmith&rsquo;s own migration wizard plus some help from me, and one was moved end to end by the skill. The early numbers for the others look good, from slightly faster to roughly twice as fast, but they rest on a handful of runs each.</p>
<h2 id="leave-the-pipeline-faster-than-you-found-it">Leave the pipeline faster than you found it<a class="gn-heading-anchor" href="#leave-the-pipeline-faster-than-you-found-it" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Nobody throws a party when CI works. When it&rsquo;s slow or flaky, everyone is annoyed as hell. It&rsquo;s a bigger part of the job than we like to admit, and it gets slow without anyone deciding it should: a few tests at a time, until one week it takes eighteen minutes and everyone has quietly started working around it.</p>
<p>So I treat it the way I treat code, with the boy scout rule: leave the pipeline better than you found it. Whenever I&rsquo;m working in a repository, I look at how long its CI takes and whether its caches work, and if something is obviously slow, I fix it while I&rsquo;m there.</p>
<p>With an agent, that costs almost nothing. I point one at the repository and let it read the past builds, find the slow steps, try a change, measure it, keep what got faster, drop what got worse, and go round again. It runs in the background while I work on bugs and features, and it barely takes any of my attention. The skill writes that loop down. I&rsquo;d like it to become everyone&rsquo;s habit, not just mine.</p>
<h2 id="back-to-the-coffee">Back to the coffee<a class="gn-heading-anchor" href="#back-to-the-coffee" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>The test suite is still growing, so one day the pipeline will be slow again. With any luck, whoever notices will fix it before the coffee is ready. Somewhere, I assume, the smokers are still making the real decisions. I just can&rsquo;t blame the build for joining them anymore.</p>
]]></content:encoded>
      <category>Engineering</category>
      <category>Infrastructure</category>
      <category>AI</category>
    </item>
    <item>
      <title>99% excitement, 1% concern</title>
      <link>https://build.golfnext.com/en/posts/culture-of-enthusiasm/</link>
      <pubDate>Thu, 03 Sep 2026 00:00:00 +0000</pubDate><dc:creator>John Allan Nielsen</dc:creator>
      <guid>https://build.golfnext.com/en/posts/culture-of-enthusiasm/</guid>
      <description>I spent a Saturday evening watching a movie while an agent rewrote one of our frontends from Vue to React. It was live a couple of days later. The interesting part isn&#39;t the AI — it&#39;s what a company has to be like for that to be allowed, the one concern management actually raised, and why an environment that says yes this fast demands more discipline, not less.</description>
      <content:encoded><![CDATA[<p>I was watching a movie on a Saturday evening with my laptop open on the table next to me. Every so often I&rsquo;d glance over, read a few lines, and go back to the film. <a href="https://code.claude.com/docs">Claude Code</a>, Anthropic&rsquo;s terminal coding agent, was rewriting the entire frontend of one of our applications from Vue to React.</p>
<p>Not a component. The frontend. Every screen the application has, which is tens of thousands of lines.</p>
<p>I&rsquo;d given it a narrow job and narrow permissions: build a proof of concept, in its own corner, touching nothing that mattered. Nobody had asked me to do this. It was a thing I wanted to know the answer to.</p>
<p>On Monday I showed it to <a href="/en/author/jb/">our tech lead</a>. He was genuinely excited, took the proof of concept into the real repository, spent a pass cleaning up its foundations, and shipped it. Within a day or two the old frontend was switched off.</p>
<p>GolfNext, where I work, builds the software golf clubs run on: bookings and memberships, the self-service kiosks that work as a reception desk when nobody&rsquo;s behind the counter, the payment side of the driving range, which is the practice area where you pay for a basket of balls and hit them. I&rsquo;d been there four weeks. The application I rewrote is one club staff sit in front of all day, so when it breaks, someone at a club is standing at a desk with a customer in front of them.</p>
<p>Before this I spent years at a much bigger company, which is most of the point.</p>
<p>A company let a four-week employee&rsquo;s weekend experiment replace a staff-facing frontend inside a week. That&rsquo;s the part I still find surprising, and the AI is the least of it.</p>
<h2 id="why-a-rewrite-at-all">Why a rewrite at all<a class="gn-heading-anchor" href="#why-a-rewrite-at-all" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Since the rest of this post spends a while on why good ideas stall elsewhere, it&rsquo;s only fair to give the reason for this one up front.</p>
<p>It isn&rsquo;t that React is better than Vue. Our mobile apps are already React Native, and if the desktop application we ship ever moves to a web interface, React would be the single framework behind every frontend at GolfNext, web or desktop. One set of idioms instead of two, one dependency tree, and one answer to a question rather than two that both half-apply.</p>
<p>The other reason is less flattering to talk about and matters more day to day: coding agents are noticeably better at React than at Vue. There is far more React in the world, so far more of it went into the training. If we&rsquo;re going to build software the way we now build it, there&rsquo;s an argument for standing where the tools are strongest.</p>
<h2 id="what-would-have-happened-at-my-last-job">What would have happened at my last job<a class="gn-heading-anchor" href="#what-would-have-happened-at-my-last-job" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Nothing, or close to it. But not for the reason you&rsquo;d probably guess.</p>
<p>My last job was a large enterprise and I want to be fair to it: it wasn&rsquo;t drowning in bureaucracy. There wasn&rsquo;t a thicket of committees to get an idea through. What there was instead was a small number of senior developers who had been there a very long time, held most of the decisions, and were comfortable with the way things already were.</p>
<p>Nothing had to be formally refused, and mostly nothing was. A proposal only had to fail to interest the two or three people whose agreement it needed, and then it quietly didn&rsquo;t happen. That&rsquo;s a harder wall than a process, because a process at least tells you what to do next.</p>
<p>The deployment story had the same shape. Releases were largely manual, and the ability to perform one — along with the access you&rsquo;d need to diagnose anything when it went wrong — sat with a handful of people. If you weren&rsquo;t one of them, your work stopped at the point where it needed something only they could do, and then it waited. Not out of malice. It was how the permissions had settled over the years, and nobody whose day it improved had a reason to change it.</p>
<p>So the honest version of the contrast isn&rsquo;t speed. It&rsquo;s how much of what you already know how to do you&rsquo;re actually permitted to do. At GolfNext, the person who reviewed my weekend experiment was also the person who could ship it. Where I worked before, that would have been two different people, and a queue.</p>
<h2 id="how-an-idea-gets-a-yes-here">How an idea gets a yes here<a class="gn-heading-anchor" href="#how-an-idea-gets-a-yes-here" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Two paths, and neither of them has an approval step.</p>
<p>The first is a conversation. Someone says the thing out loud, whoever&rsquo;s in the room agrees it&rsquo;s worth doing, and it gets done. No document. The second is a task in our tracker, which is the same thing with a title and an assignee so it doesn&rsquo;t get lost.</p>
<p>There&rsquo;s no third path where the idea goes away to be evaluated. Worth being precise about what that does and doesn&rsquo;t mean: the gate we don&rsquo;t have is the one that decides whether an idea is <em>allowed to be tried</em>. Code review and the release process are still exactly where you&rsquo;d expect them.</p>
<p>Most of the ideas come from our founder, who started the company in 2012. He has more of them in a week than the team can build in a month, which could easily be exhausting and mostly isn&rsquo;t, because the flow runs in both directions and an idea from a developer gets the same reception his do. I&rsquo;ve brought several since I started, and not one of them got the treatment I was braced for.</p>
<p>Our tech lead wrote something close to this on this blog two weeks into his own job — he&rsquo;d arrived braced for friction and found almost none of it. Four weeks in, from a different chair, I&rsquo;d say the same. The difference is that I&rsquo;ve now got a story concrete enough to argue with.</p>
<p>The reception isn&rsquo;t just approval either, and that&rsquo;s the part I hadn&rsquo;t expected. Show something to our founder, our product manager or our CEO, and inside a minute one of them is building on it out loud: what could sit on top of this, what else it could be used for, where the value to the company would actually land. You arrive with a thing you made and leave with three things it could become. That&rsquo;s a good feeling, and it&rsquo;s also a reason to turn up prepared: an idea here grows the moment you show it, so it pays to have thought about where it could go before somebody else does.</p>
<p>The turnaround is the part I still haven&rsquo;t adjusted to. Sometimes an idea in the morning is live within twenty-four hours. Sometimes it&rsquo;s two weeks. It&rsquo;s rarely a quarter.</p>
<h2 id="the-part-i-dont-want-to-gloss-over">The part I don&rsquo;t want to gloss over<a class="gn-heading-anchor" href="#the-part-i-dont-want-to-gloss-over" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>&ldquo;An AI rewrote a staff-facing frontend and we shipped it two days later&rdquo; is either good engineering or a story that ends badly.</p>
<p>So, the details.</p>
<p>The agent could create, and that was the whole of it: no editing or deleting anything that already existed, all of it off in its own corner. Nothing it produced was one merge away from a golf club&rsquo;s Monday morning.</p>
<p>Then a human read it. Our tech lead clicked through every view — there aren&rsquo;t that many, twenty or thirty with some forms and modals, half an hour of clicking — and concluded it looked good enough to replace the old application without breaking anything. What he didn&rsquo;t have was years of history with this codebase; he&rsquo;d been there about as long as I had. So the review wasn&rsquo;t institutional memory. It was somebody who knows what a healthy React codebase looks like reading this one before it went anywhere near a club.</p>
<p>Where he did spend his time was the foundation rather than the behaviour: getting the look back closer to the application we were replacing, putting Biome in, settling how directories and files get named, and taking apart the places where the agent had stacked fifty Tailwind classes onto a single div — restructured, then a lint rule so it can&rsquo;t come back. A human should still be able to read the thing. That&rsquo;s the work that makes it safe to add functionality to, instead of building the next floor onto something already leaning like the Tower of Pisa.</p>
<p>That review is the actual safety mechanism in this story, not the model and not my prompt. Not the tests either, because this frontend never had much of a suite. That was already true before the rewrite. The rewrite just made it matter more.</p>
<p>It was also frontend-only. Same API, same data, nothing touched on the payment path.</p>
<p>What we don&rsquo;t have is a gradual rollout, and this blog has already admitted as much: our customers are physical places, so the first ring is geography, and if a release goes badly at a nearby club the recovery plan is twenty minutes in a car. That&rsquo;s a real safety net, and I&rsquo;m not going to call it a canary deployment.</p>
<p>The thing I&rsquo;m still carrying is that nobody has read every line of that code the way you read code you typed yourself. A rewrite that looks finished is the most flattering output an agent produces, because the gaps are omissions rather than bugs, and an omission doesn&rsquo;t show up in a demo. I expect to keep finding them for a while. So far nothing has broken at a club.</p>
<h2 id="the-plan-the-weekend-belonged-to">The plan the weekend belonged to<a class="gn-heading-anchor" href="#the-plan-the-weekend-belonged-to" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Nobody had asked for that rewrite. It was the first piece of something longer I&rsquo;d been sketching, and what decided the timing was mundane: I looked at how much of my Claude Code allowance was left for the month. Unused tokens don&rsquo;t roll over. Capacity I hadn&rsquo;t spent was capacity GolfNext had already paid for, and spending it cost the company nothing at all.</p>
<p>So I spent it. Over that stretch I started proofs of concept for most of our applications, all of them in one language with one code structure, to find out what a unified stack would actually look like instead of arguing about it in the abstract. Exactly one of them, the React frontend, has reached production. The rest are still proofs of concept, and I&rsquo;d rather say that plainly than let this read as a migration that&rsquo;s already happened.</p>
<p>We run two admin portals, each with its own backend and its own login. They serve different purposes, but there&rsquo;s no good reason for them to be separate applications, and I genuinely don&rsquo;t know why they were built that way — it predates me by years. What I do know is that every developer here and everyone in management agrees there should be one. The cost of having two lands on the people using them: club staff log in twice.</p>
<p>The plan is to end up with one. First an auth bridge, so a single set of credentials works across both. Then the merge itself: one frontend and one navigation, with both backends still running behind it. Only then the backend, replacing the two services with one, so there&rsquo;s a single language and a single code structure under the whole portal instead of two of each.</p>
<p>There&rsquo;s a reason that road is worth walking now rather than in two years. The ambition here isn&rsquo;t modest. We&rsquo;re in eighteen countries today, the plan involves a good many more, and a lot more users behind them. Much of that arrives through integrations: every club already runs some management system, and our position is to work with whatever they have rather than ask them to throw it out. Each one of those is another system to speak to and another set of assumptions to keep straight.</p>
<p>Two portals, two backends and two authentication schemes is survivable at the size we are now. It&rsquo;s exactly the kind of thing that stops being survivable when things go well. So the argument for doing the unglamorous structural work is that there&rsquo;s still slack to do it in, and that&rsquo;s also why &ldquo;let&rsquo;s unify the stack&rdquo; is met with excitement here rather than a request to justify it. Everybody already knows what&rsquo;s coming.</p>
<p>That&rsquo;s a long road rather than a sprint, and most of it hasn&rsquo;t happened yet. Getting the frontend onto React was the groundwork for all of it, the thing that had to be true before any of those steps could start, which is why that was the experiment I picked for a Saturday evening. Step two was showing the whole plan to the people who would have to live in it.</p>
<h2 id="where-the-1-went">Where the 1% went<a class="gn-heading-anchor" href="#where-the-1-went" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Our tech lead was pushing in the same direction and wanted the rest of our frontends on React too. Management&rsquo;s reaction to the whole package — the portals, the backend, the frontends — was, in round numbers, 99% excitement and 1% concern.</p>
<p>I want to be precise about the 1%, because it wasn&rsquo;t what I expected. I&rsquo;d braced for cost, or for risk to customers. What they were worried about was how the existing developers would feel.</p>
<p>These are people who have been building this software for years, in a stack they chose, and two of us — both of us about a month in the door — were proposing to change it. That&rsquo;s the right thing to be concerned about, and it&rsquo;s the concern that never shows up in an estimate.</p>
<p>So we ran a brainstorm with the team, and held our own opinions back until everyone else had spoken.</p>
<p>That&rsquo;s harder than it sounds. We already had our proposal written down. Every time somebody drifted towards it the tempting move was to jump in and agree, which would have ended the discussion and turned everything after it into consent rather than input.</p>
<p>When we finally put ours on the table, nobody disagreed. The portal merge and what follows it they were actively excited about. The React standardisation itself they were only fine with, and that&rsquo;s the more interesting half of the sentence: it&rsquo;s the part that replaces the stack they had chosen, and &ldquo;fine with&rdquo; strikes me as an entirely reasonable thing to feel about that. Nothing in the proposal changed as a result, which is the least satisfying way for a meeting to go well, and I&rsquo;d rather report that than dress it up. We&rsquo;re working in that direction now.</p>
<p>Enthusiasm from management is easy to come by here. That&rsquo;s a feature, and it&rsquo;s most of why the twenty-four-hour turnaround exists at all. Enthusiasm from the people who have to live in the code every day is the one you have to earn, and the only reliable way I know to earn it is to shut up for the first half of the meeting.</p>
<h2 id="enthusiasm-asks-for-discipline">Enthusiasm asks for discipline<a class="gn-heading-anchor" href="#enthusiasm-asks-for-discipline" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>I don&rsquo;t want to leave this as a post about how nice it is here, so let me say what the environment actually demands, because it isn&rsquo;t nothing.</p>
<p>A place that says yes in twenty-four hours only works if what you put in front of it deserves a yes in twenty-four hours. None of the proposals in this post were sketched in the meeting they were presented in. Our tech lead and I have spent a lot of hours together on plans and architecture, going through them properly before anyone else saw them, and separately we each did our own reading first. Most ideas I&rsquo;ve brought here were researched before they were presented, not while they were being discussed.</p>
<p>The weekend proof of concept is the clearest case. Its purpose wasn&rsquo;t to produce code. It was to take the riskiest assumption in a plan and settle it before asking anybody to commit to the plan. That&rsquo;s preparation dressed up as a hobby.</p>
<p>This is the part I&rsquo;d underline for anyone who thinks an enthusiastic culture sounds comfortable. When there&rsquo;s no gatekeeper you have to convince, there&rsquo;s also nobody checking your thinking for you. The rigour a process would have forced on you has to come from you instead, and it has to arrive before the meeting rather than after it. Enthusiasm doesn&rsquo;t lower the bar for professionalism. It moves the bar onto you.</p>
<p>Which makes this a particular kind of place to work rather than a straightforwardly better one. If you&rsquo;re ambitious, and you enjoy doing the preparation properly, it&rsquo;s the best environment I&rsquo;ve worked in — what you can get done is bounded by your own thinking rather than by somebody&rsquo;s approval queue. If you&rsquo;d rather have a process tell you what&rsquo;s next and a committee share the weight of being wrong, I think you&rsquo;d find it uncomfortable.</p>
<p>If you&rsquo;re weighing up a place like this: the fastest thing about GolfNext isn&rsquo;t the tooling or the agents. It&rsquo;s the distance between having an idea and being allowed to try it.</p>
<p>You do have to be careful what you think about on a Saturday evening. It might be in production by Tuesday.</p>
]]></content:encoded>
      <category>Culture</category>
      <category>Engineering</category>
      <category>AI</category>
    </item>
    <item>
      <title>My brother from another mother?</title>
      <link>https://build.golfnext.com/en/posts/brother-from-another-mother/</link>
      <pubDate>Wed, 02 Sep 2026 09:00:00 +0000</pubDate><dc:creator>John Allan Nielsen</dc:creator>
      <guid>https://build.golfnext.com/en/posts/brother-from-another-mother/</guid>
      <description>I started a job where the other half of the technical side had begun the day before me, with a title next to mine and no line drawn between them. A stranger, as new as I was, and the two of us apparently expected to steer the same team. What I was actually worried about, and what a month has and hasn&#39;t settled.</description>
      <content:encoded><![CDATA[<p>I interviewed for the job <a href="/en/author/jb/">Johann</a> has.</p>
<p>Tech Lead at GolfNext — the company that builds the software golf clubs run on: booking a round, the till in the pro shop, the machines out on the practice range. It has been doing that for over a decade, mostly with developers working remotely, and the team that has kept it running has been here far longer than either of us.</p>
<p>I went through the process and I wanted the role. The feedback arrived as two things at once: they were going with somebody else, and they wanted to create a new role for me, because they had been impressed enough not to let it end there. That is not a rejection you get very often.</p>
<p>So the man who got the job I&rsquo;d applied for became the man I would be working next to. He started on the Monday. I started on the Tuesday — one day behind, which is close enough to zero.</p>
<p>His title is Tech Lead. Mine is Solution Architect and Technical Project Manager. Read those two out loud and try to say where one stops and the other starts. Nobody had drawn that line, and as far as I can tell nobody was planning to.</p>
<p>So I turned up to a job I was genuinely excited about, and inside the excitement was a knot I didn&rsquo;t say out loud for a while.</p>
<p>On our second day working together, Johann told me we were like twins. He has said it many times since, and it&rsquo;s become a running joke between us — the title of this post is my half of it. But I want to be honest about what came before the joke.</p>
<h2 id="what-i-was-actually-worried-about">What I was actually worried about<a class="gn-heading-anchor" href="#what-i-was-actually-worried-about" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Four questions, in the order they kept arriving.</p>
<p><strong>Would we agree?</strong> Not about tabs and spaces. About the things you can&rsquo;t do twice: which stack we standardise on, whether you rewrite or refactor, what gets rebuilt first when everything has a claim to being first.</p>
<p><strong>Could we work together at all?</strong> I had never met him. Not a former colleague, not a friend of a friend, not someone whose work I&rsquo;d seen. A stranger with an adjacent mandate and the same start month.</p>
<p><strong>What happens the first time we fundamentally disagree?</strong> Two people with overlapping remits and no boundary is fine while they agree. It&rsquo;s a coin toss the day they don&rsquo;t, and there&rsquo;s no process to appeal to, because there is no process.</p>
<p><strong>And the one I didn&rsquo;t say out loud: was one of us supposed to quit?</strong> We had been candidates for the same chair a few weeks earlier, and one of us hadn&rsquo;t got it. Companies do sometimes hire two senior people into adjacent roles and let the situation resolve itself. Nobody told me that was happening. Nobody told me it wasn&rsquo;t, either, and in week one you don&rsquo;t have the standing to ask.</p>
<h2 id="why-that-wasnt-paranoia">Why that wasn&rsquo;t paranoia<a class="gn-heading-anchor" href="#why-that-wasnt-paranoia" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Two simultaneous senior hires into a small team is an unusual move, and it isn&rsquo;t obviously a good one. The team already had people who had maintained this software for years. Now it also had two newcomers, arriving a day apart, both of whom were going to have opinions about the technical direction — and neither of whom had earned a single hour of credibility inside the building.</p>
<p>I also came from somewhere that taught me what an unresolved question about who decides does to a codebase over time. I&rsquo;ve watched decisions collect around whoever has been there longest, and I&rsquo;ve watched perfectly good ideas quietly fail to happen because they never interested the right two people. Arriving into a place where the deciding was undefined between me and a man I&rsquo;d never met, my instinct wasn&rsquo;t optimism. It was: this could go badly, and it could go badly slowly, which is worse.</p>
<p>One thing did work in my favour, and I didn&rsquo;t expect it. He&rsquo;d already published <a href="/en/posts/letter-day-one/">a letter to himself</a> on this blog, before his own first day, laying out what he was walking into and the goals he was setting himself. I&rsquo;ve read it since. Reading somebody&rsquo;s stated compass before you&rsquo;ve had a single real conversation with them is a strange kind of head start — you know what they say they want to be like, and you get to watch whether it holds. One line in it does most of the work: <em>hire excellent people, and then trust them.</em> You can&rsquo;t write that in public and then spend your first month collecting territory.</p>
<h2 id="a-month-in">A month in<a class="gn-heading-anchor" href="#a-month-in" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>We have spent a lot of the last month in front of the same problems: what this estate actually consists of, what has to be true before anything else can happen, what the first real move should be.</p>
<p>The uncomfortable part is that it keeps going the same way. He describes the shape of a problem and it&rsquo;s the shape I&rsquo;d have drawn. I bring what I think the first step has to be and it&rsquo;s the step he&rsquo;d already picked. Two people forming the same picture of a company neither of them knew a month ago, independently, is either a very good sign or a shared blind spot, and from the inside those look identical.</p>
<p>It also made the credibility question smaller in a way I hadn&rsquo;t predicted. One newcomer with an opinion about the direction is a newcomer with an opinion. Two newcomers who arrived separately and reached the same conclusion is at least an argument.</p>
<p>So the honest report from the first month is that the thing I was braced for hasn&rsquo;t happened, and the alignment has been close enough to be faintly suspicious.</p>
<h2 id="what-happens-instead-of-disagreement">What happens instead of disagreement<a class="gn-heading-anchor" href="#what-happens-instead-of-disagreement" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Within a few days we were quizzing each other on preferences for more or less everything. Tools. Branching. How much process a small team should carry. When you rewrite something and when you leave it alone. What a good pull request looks like. How you treat an estate you didn&rsquo;t build. The answers kept matching — not roughly, almost every time.</p>
<p>And when we actually work on something, it isn&rsquo;t a collision. One of us pitches an idea, the other comes back with a suggestion that makes it better, and that version is the one we take. I honestly don&rsquo;t know whether that counts as disagreeing, and I&rsquo;ve decided not to care what it&rsquo;s called, because what comes out the other end is better than what either of us walked in with.</p>
<p>I&rsquo;m not going to pretend that settles the hard case. A month is not long, and I can&rsquo;t report from a conflict I haven&rsquo;t had. But it would be overcorrecting to describe what&rsquo;s happened so far as conflict merely deferred. It looks like two people upgrading each other&rsquo;s work, which is a different thing entirely.</p>
<p>The part I hadn&rsquo;t accounted for is how much easier the job is with a colleague who is exactly as ambitious as you are and pointed in the same direction. I&rsquo;d braced for a rival. What I have is the person I check my thinking against before it leaves the building.</p>
<h2 id="different-angles-same-target">Different angles, same target<a class="gn-heading-anchor" href="#different-angles-same-target" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>The overlap I was worried about hasn&rsquo;t materialised either, and not because anybody negotiated it away. We never sat down and divided the territory. It divided itself, by angle of attack.</p>
<p>In practice that means we are usually pointed at the same problem from different sides. One of us is deep in security while the other rewrites the architecture for a big project. One of us takes a large new feature on the payments side while the other is down in a hardware integration. Same destination, two approaches, and almost no surface where we get in each other&rsquo;s way.</p>
<p>What surprises me is how little coordinating that takes. There&rsquo;s no standing sync where we carve up the week. We each know roughly what the other is doing and, more importantly, what the other is doing it <em>for</em>, so the split falls out of the work instead of out of a meeting. I&rsquo;d have predicted the opposite: two people with undefined remits should need constant coordination to stay out of each other&rsquo;s way.</p>
<p>And that, I think, is what keeps the twins joke alive without either of us maintaining it. We keep arriving at the same conclusion from opposite directions.</p>
<h2 id="was-one-of-us-supposed-to-quit">Was one of us supposed to quit?<a class="gn-heading-anchor" href="#was-one-of-us-supposed-to-quit" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>No. And the longer I look at how it was handled, the more I think management made the right call.</p>
<p>Two people came out of that process. Instead of taking one and sending the other away, they took both and built a role for the second — which only makes sense if you are honest about the size of what needs doing. There are more repositories in this company than there are weeks in a year, four products serving real venues, and a decade of technology decisions that were all reasonable when they were made and don&rsquo;t add up to one coherent estate now. Every part of that needs somebody with time to think about it, and I have not once, in a month, run into a shortage of things to own.</p>
<p>So the thing that looked from the outside like two people competing for one chair turned out to be a company deciding it needed two. The knot I arrived with came from reading it the first way. A month in, the part I&rsquo;d have called the risk — the two of us — is the part working best.</p>
<h2 id="so-brother-from-another-mother">So: brother from another mother?<a class="gn-heading-anchor" href="#so-brother-from-another-mother" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Provisionally, yes — with the question mark left where it is on purpose.</p>
<p>A month of turning out to think the same way about a company we both just met is genuinely the best professional surprise I&rsquo;ve had in years. It&rsquo;s also only a month. The thing that would actually earn the phrase is the first decision only one of us can make, handled well, with the team watching. Ask me again after that.</p>
<p>What I&rsquo;d say to anyone walking into the same setup: the worry is reasonable and worth naming out loud earlier than I did. Two senior people with no boundary between them is a real risk, and <em>we get on</em> is not a plan for it. But the failure mode I&rsquo;d braced for — two newcomers quietly competing over the same ground — needs a shortage to feed on. In a place with this much undone, there isn&rsquo;t one.</p>
<h2 id="one-dissent-for-the-record">One dissent, for the record<a class="gn-heading-anchor" href="#one-dissent-for-the-record" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>I should correct something, in the interest of accuracy. There is exactly one thing we disagree on.</p>
<p>Our task tracker and our wiki. My position is that they are sub par. Johann&rsquo;s is that they are absolutely horrible. We have been over it more than once, from opposite directions as usual, and we arrive at the same destination — something has to change — by way of a genuine and so far unresolved difference of opinion about how much contempt is warranted on the way there.</p>
<p>So the record stands. Two newcomers, one undrawn boundary, a month of agreeing about architecture, process, rewrites, branching and what makes a good pull request. And one open dispute, about adjectives.</p>
]]></content:encoded>
      <category>Culture</category>
    </item>
    <item>
      <title>Delete the setup guide</title>
      <link>https://build.golfnext.com/en/posts/delete-the-setup-guide/</link>
      <pubDate>Thu, 27 Aug 2026 00:00:00 +0000</pubDate><dc:creator>Johann Böhler</dc:creator>
      <guid>https://build.golfnext.com/en/posts/delete-the-setup-guide/</guid>
      <description>Every repository ships instructions nobody keeps up to date, and a .env.example somebody has to fill in from a Slack message. Two tools — mise for the toolchain, fnox for the secrets — take both jobs out of the README and put them in the repo, with a fair account of what that costs and what it doesn&#39;t fix.</description>
      <content:encoded><![CDATA[<p>Think about the last time you cloned a repository you&rsquo;d never worked in before.</p>
<p>You read the README. It told you to install a JDK, and you had the wrong one, so you went and found the right one. It told you to install Node, and the version file said 18 while the lockfile clearly wanted 20. Somewhere near the bottom there was a line saying <code>cp .env.example .env</code> and then, in a smaller font, <em>fill in the values</em>. Fill them in from where? From a colleague. So you asked in Slack, someone pasted you a block of <code>KEY=value</code> lines, and now a production-adjacent credential lives in a chat history forever: searchable, backed up, exported to whatever compliance tool the company bought last year.</p>
<p>Nobody set out to build that. It&rsquo;s what happens when the instructions live in a document instead of in the repository.</p>
<p>I&rsquo;m the tech lead at GolfNext, a Danish company that builds software for golf clubs, and my first post on this blog was an inventory of what I&rsquo;d walked into. Sixty-eight repositories, four product lines, thirteen years of accumulated technology choices: Java and Maven and Spring, Kotlin and Gradle, Node, Python, PHP, JavaFX, React Native. Starting work in one of them cold took somewhere between two and six hours depending which one you picked, and I set myself a target of five minutes from clone to running code.</p>
<p>This is the first serious attempt at that. Two tools, both by the same person, jdx. <a href="https://mise.jdx.dev/">mise</a> puts the right tools on your PATH, <a href="https://fnox.jdx.dev/">fnox</a> puts the right secrets in your environment, and those are the two halves of one problem. mise is boring and widely used. fnox is neither of those things yet, and I&rsquo;ll come back to that. What&rsquo;s interesting is what falls away once both are in place, and what it does to the coding agents I have running in these repositories all day.</p>
<p>It&rsquo;s <em>mise</em> as in <em>mise en place</em>, the kitchen thing. Pronounced &ldquo;meez&rdquo;, /miːz/, one syllable, no <em>-uh</em> on the end. You want that settled before you say it out loud in a meeting. fnox is easier: Fort Knox with most of the letters knocked out, so &ldquo;eff-nox&rdquo;, and the name tells you what it&rsquo;s for.</p>
<h2 id="half-one-the-tools">Half one: the tools<a class="gn-heading-anchor" href="#half-one-the-tools" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>If you&rsquo;ve used nvm or pyenv you know the shape: declare a version, <code>cd</code> in, get that version. The difference is that mise doesn&rsquo;t care what the version is <em>of</em>.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="c"># .mise.toml</span>
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="nx">tools</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">java</span> <span class="p">=</span> <span class="s2">&#34;21&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">maven</span> <span class="p">=</span> <span class="s2">&#34;latest&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">node</span> <span class="p">=</span> <span class="s2">&#34;lts&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">python</span> <span class="p">=</span> <span class="s2">&#34;latest&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">gitleaks</span> <span class="p">=</span> <span class="s2">&#34;latest&#34;</span>
</span></span></code></pre></div><p>That&rsquo;s a JVM, a build tool, a JavaScript runtime, a Python and a secret scanner (<a href="https://github.com/gitleaks/gitleaks">gitleaks</a>) in one file, installed by one command, on your PATH only inside this directory tree. Add <code>mise activate</code> to your shell profile and even the command goes away: <code>cd</code> into the repo and the tools are there, <code>cd</code> out and they aren&rsquo;t. It also sets the environment variables that go with them, so <code>JAVA_HOME</code> points at the JDK this project pinned rather than whatever your operating system decided last.</p>
<p>&ldquo;Version manager&rdquo; undersells this, and I want to be specific about why. The expensive part of a polyglot estate is the switching, not any single stack. You spend a morning in a Spring service, then move to an Android repo, then a small Python tool, and each hop is a chance for your shell to be wrong in a way that produces a confusing error rather than an obvious one. In one of our repositories a newer system JDK doesn&rsquo;t fail loudly. It breaks a mocking library during tests, several layers down, with a message that has nothing to do with Java versions. That&rsquo;s an hour of somebody&rsquo;s life, and you pay it again every time a new person joins or a laptop gets replaced.</p>
<p>A per-language tool can&rsquo;t help, because the problem is <em>between</em> languages.</p>
<p>One nuance you may as well steal instead of discovering. We deliberately let some of those versions float: <code>latest</code> for the scanner, <code>lts</code> for Node. What a <em>build</em> is made of is pinned in the CI workflow, and that pin is the one dependency automation watches and raises pull requests against. If we pinned the same versions in <code>.mise.toml</code> as well, we&rsquo;d have two places to keep in step, and one of them would quietly fall behind. We know this because it had already happened, a <code>.mise.toml</code> claiming Node 22 while all four workflows said 24. Only one file gets to be authoritative, and it&rsquo;s the workflow.</p>
<p>The JDK is the exception, pinned hard in both places on purpose. A wrong major version there is exactly the mocking-library failure above, and I&rsquo;d rather keep two files in step than rediscover that error at four in the afternoon. Everything else floats.</p>
<h2 id="the-part-i-didnt-expect-to-care-about">The part I didn&rsquo;t expect to care about<a class="gn-heading-anchor" href="#the-part-i-didnt-expect-to-care-about" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Every repository has a handful of things you do to it: build it, test it, run it, format it. In a Maven project that&rsquo;s <code>mvn spring-boot:run -Dspring-boot.run.profiles=local</code>. In a Gradle project it&rsquo;s <code>./gradlew</code> and a task name with four words glued together. In a Node project it&rsquo;s whatever the <code>scripts</code> block says this week. None of it is hard. All of it is something you have to look up when you&rsquo;re in an unfamiliar repo, which is exactly when you have the least patience for looking things up.</p>
<p>mise lets you put those verbs in the same file as the tools:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="p">[</span><span class="nx">tasks</span><span class="p">.</span><span class="nx">build</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">description</span> <span class="p">=</span> <span class="s2">&#34;Package the artifact&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">run</span> <span class="p">=</span> <span class="s2">&#34;mvn clean package -B&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="nx">tasks</span><span class="p">.</span><span class="nx">test</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">description</span> <span class="p">=</span> <span class="s2">&#34;Full test suite (integration tests need Docker)&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">run</span> <span class="p">=</span> <span class="s2">&#34;mvn clean test -B&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="nx">tasks</span><span class="p">.</span><span class="nx">serve</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">description</span> <span class="p">=</span> <span class="s2">&#34;Run the API on http://127.0.0.1:5001 (starts the stack first)&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">depends</span> <span class="p">=</span> <span class="p">[</span><span class="s2">&#34;stack:up&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">run</span> <span class="p">=</span> <span class="s2">&#34;mvn spring-boot:run -Dspring-boot.run.profiles=local&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="nx">tasks</span><span class="p">.</span><span class="s2">&#34;stack:up&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">description</span> <span class="p">=</span> <span class="s2">&#34;Start the local database and the mocks. Idempotent&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">run</span> <span class="p">=</span> <span class="s2">&#34;docker compose up -d --wait&#34;</span>
</span></span></code></pre></div><p>So the interface becomes <code>mise run build</code>, <code>mise run test</code>, <code>mise run serve</code>. In the Android repository next door the same verbs sit on top of Gradle instead, and one of them wraps a four-word task name I still can&rsquo;t type from memory. Doesn&rsquo;t matter. <code>mise tasks</code> lists what a repository can do, with descriptions, and the answer is in the repo rather than in a wiki page that stopped being true in 2023.</p>
<p>Two small conventions have earned their place. A bare verb is the common case and <code>verb:qualifier</code> narrows it: <code>test</code> and <code>test:signer</code>, <code>stack:up</code> and <code>stack:down</code> and <code>stack:clean</code>. There&rsquo;s also always an escape hatch task that wraps the underlying build tool directly, so anything the curated list doesn&rsquo;t cover is still one command away:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="p">[</span><span class="nx">tasks</span><span class="p">.</span><span class="nx">mvn</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">description</span> <span class="p">=</span> <span class="s2">&#34;Any Maven command, with this repo&#39;s JDK and environment&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">run</span> <span class="p">=</span> <span class="s2">&#34;mvn&#34;</span>
</span></span></code></pre></div><p>That escape hatch matters for the second half of the story.</p>
<h2 id="half-two-the-secrets">Half two: the secrets<a class="gn-heading-anchor" href="#half-two-the-secrets" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>This is the part I actually wanted to fix.</p>
<p>The <code>.env.example</code> pattern is so common that it reads as a best practice. It&rsquo;s a convention that gave up. Look at what it asks of a team.</p>
<p>A new developer copies a file of empty keys and then has to source real values from somewhere the repository can&rsquo;t tell them about. In practice that means a person, which means a chat message, and now a secret exists in a second system with different retention rules and different access control than the one you chose on purpose. When a value rotates, every laptop holding the old one is silently wrong until someone notices, and the failure usually shows up as a confusing 401 rather than a message saying &ldquo;your credential is four months stale&rdquo;. When a new secret is added, everyone else&rsquo;s <code>.env</code> is incomplete and nobody is told. And the file itself sits in your working directory in plaintext, one bad <code>git add -A</code> away from being committed, which is why every repository that uses this pattern also has a <code>.gitignore</code> line that somebody could delete.</p>
<p>The alternative isn&rsquo;t complicated. Put the values in a secret store you already trust, put <em>references</em> to them in the repository, and let each developer&rsquo;s own identity decide whether they resolve.</p>
<p>That&rsquo;s fnox. Here&rsquo;s the stage config for a service, committed, with no values anywhere in it:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="c"># fnox.stage.toml</span>
</span></span><span class="line"><span class="cl"><span class="nx">env</span> <span class="p">=</span> <span class="s2">&#34;exec&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="nx">providers</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">store</span> <span class="p">=</span> <span class="p">{</span> <span class="nx">type</span> <span class="p">=</span> <span class="s2">&#34;…&#34;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="nx">secrets</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">DB_PASSWORD</span>    <span class="p">=</span> <span class="p">{</span> <span class="nx">provider</span> <span class="p">=</span> <span class="s2">&#34;store&#34;</span><span class="p">,</span> <span class="nx">value</span> <span class="p">=</span> <span class="s2">&#34;DB_PASSWORD&#34;</span><span class="p">,</span>    <span class="nx">if_missing</span> <span class="p">=</span> <span class="s2">&#34;error&#34;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="nx">PAYMENT_API_KEY</span> <span class="p">=</span> <span class="p">{</span> <span class="nx">provider</span> <span class="p">=</span> <span class="s2">&#34;store&#34;</span><span class="p">,</span> <span class="nx">value</span> <span class="p">=</span> <span class="s2">&#34;PAYMENT_API_KEY&#34;</span><span class="p">,</span> <span class="nx">if_missing</span> <span class="p">=</span> <span class="s2">&#34;error&#34;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="nx">SIGNING_KEY</span>    <span class="p">=</span> <span class="p">{</span> <span class="nx">provider</span> <span class="p">=</span> <span class="s2">&#34;store&#34;</span><span class="p">,</span> <span class="nx">value</span> <span class="p">=</span> <span class="s2">&#34;SIGNING_KEY&#34;</span><span class="p">,</span>    <span class="nx">if_missing</span> <span class="p">=</span> <span class="s2">&#34;error&#34;</span> <span class="p">}</span>
</span></span></code></pre></div><p>The <code>type</code> is the only line that&rsquo;s specific to you, and I&rsquo;ve left it blank on purpose, because which store you use is the least interesting decision in the whole setup. fnox speaks to <a href="https://developer.hashicorp.com/vault">Vault</a>, <a href="https://developer.1password.com/">1Password</a>, <a href="https://github.com/getsops/sops">SOPS</a>, and the managed secret stores of the big clouds. We took the one our provider already gave us, for the boring reason that we were paying for it and it already had access control we trusted. Everything else in the file is the same whichever way you go.</p>
<p>What that file buys:</p>
<p>The repository documents its own secrets. Every name a service needs is listed in one place, in git, with review history. New secret? It&rsquo;s in the file, it arrives with <code>git pull</code>, and the next run either finds it or fails loudly because of that <code>if_missing = &quot;error&quot;</code>. Nobody has to be told anything.</p>
<p>Rotation stops being an event. Change the value in the store; every machine picks it up on its next run. There&rsquo;s no fleet of stale copies.</p>
<p>Access goes back to being an authorization question. Whether you can run against a given environment is decided by your identity in the secret store, not by whether someone remembered to forward you a file. Revoking someone becomes a permissions change instead of a hope that they delete a file.</p>
<p>And there&rsquo;s a dry run. <code>fnox check</code> tells you whether every declared secret resolves for you, without printing any of them. It&rsquo;s the fastest possible answer to &ldquo;is it me or is it broken&rdquo;, which is the question that eats onboarding days.</p>
<p>Then one deliberate restraint. That <code>env = &quot;exec&quot;</code> line means secrets are never injected into an interactive shell, only into the environment of a child process fnox launches. fnox will happily do the other thing; there&rsquo;s a mode that puts everything into your shell the moment you enter the directory. We don&rsquo;t use it. I don&rsquo;t want every process started from that directory to inherit production-adjacent credentials, and that includes editors, and it includes the coding agents I have running in there most of the day. Scoping secrets to <code>fnox exec -- &lt;command&gt;</code> is the whole difference between &ldquo;the build can see the signing key&rdquo; and &ldquo;everything on my machine can see the signing key&rdquo;.</p>
<p>That last part is what I&rsquo;d underestimated. With nothing on disk and nothing in the shell, a secret only exists inside a process that something deliberately started, which turns &ldquo;what can the agent reach&rdquo; into an ordinary permission decision. Deny it <code>fnox</code> and the handful of tasks that call it, allow it everything else, and it can read the repository, build, test and format without a credential ever entering its context.</p>
<p>Then the hole, which I&rsquo;d rather name than have someone else name for me. fnox reads the store as <em>me</em>, using credentials that are sitting on my machine regardless. An agent with a shell doesn&rsquo;t need fnox at all; it can query the store with the provider&rsquo;s own CLI. Denying <code>fnox</code> on its own closes the front door of a building with no walls. What does the real work is that the same allowlist doesn&rsquo;t include that CLI, and that what&rsquo;s within reach from a dev machine is stage rather than production. An agent that can edit <code>.mise.toml</code> can also write itself a task that shells out to anything. The deny rule is a fence, not a proof.</p>
<p>But hold it against what it replaces: a <code>.env</code> file sitting in the working directory in plaintext, readable by anything that can call <code>cat</code>, which is every agent anyone has ever run. Runtime-only injection plus a deny rule is, as far as I can tell, about as good as this currently gets.</p>
<h2 id="where-the-two-halves-meet">Where the two halves meet<a class="gn-heading-anchor" href="#where-the-two-halves-meet" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Individually these are two decent tools. Together they remove a category of manual step.</p>
<p>Because mise tasks are just commands, a task can be <code>fnox exec -- something</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="p">[</span><span class="nx">tasks</span><span class="p">.</span><span class="nx">gradle</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">description</span> <span class="p">=</span> <span class="s2">&#34;The escape hatch from earlier, now with the build secrets loaded&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">run</span> <span class="p">=</span> <span class="s2">&#34;fnox exec -- ./gradlew&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="nx">tasks</span><span class="p">.</span><span class="nx">secrets</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">description</span> <span class="p">=</span> <span class="s2">&#34;Check that every build secret resolves for you&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">run</span> <span class="p">=</span> <span class="s2">&#34;fnox check&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="nx">tasks</span><span class="p">.</span><span class="s2">&#34;serve:stage&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">description</span> <span class="p">=</span> <span class="s2">&#34;Run locally against stage secrets (needs cloud access)&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">depends</span> <span class="p">=</span> <span class="p">[</span><span class="s2">&#34;stack:up&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">run</span> <span class="p">=</span> <span class="s2">&#34;fnox -c fnox.stage.toml exec -- mvn spring-boot:run&#34;</span>
</span></span></code></pre></div><p>Now <code>mise run gradle :app:assembleRelease</code> is a signed release build, and the person running it never types the word fnox, never learns which of the build&rsquo;s inputs are secret, and never holds a copy of any of them. Nobody has to remember which commands need secrets. The task knows.</p>
<p>There&rsquo;s one in there for anyone who lives in an IDE:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="p">[</span><span class="nx">tasks</span><span class="p">.</span><span class="nx">ide</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">description</span> <span class="p">=</span> <span class="s2">&#34;Launch the IDE with the build secrets in its environment (macOS)&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">run</span> <span class="p">=</span> <span class="s1">&#39;fnox exec -- open -na &#34;Android Studio&#34; --args &#34;$PWD&#34;&#39;</span>
</span></span></code></pre></div><p>An IDE knows nothing about any of this, so a build started from inside it can&rsquo;t sign. Launch the IDE <em>through</em> fnox and its Gradle inherits the environment. It&rsquo;s one line, and it&rsquo;s the difference between a working release build and a signing error nobody can explain, so it stays in the file. I don&rsquo;t reach for it much myself: most of my day is a terminal, a prompt and an agent doing the typing. That&rsquo;s a preference, not a recommendation, and the task is there for whoever wants the other thing.</p>
<p>Then there&rsquo;s the split by environment. One file each, the same variable names in all of them, different values behind those names:</p>
<pre tabindex="0"><code>fnox.toml         # local
fnox.stage.toml   # stage
fnox.prod.toml    # production
</code></pre><p>A task points at the file it needs. Anything that belongs to an environment goes in that environment&rsquo;s file, so nobody can pair stage credentials with local settings by accident.</p>
<p>The local one is the file I&rsquo;d point at if you only read one. It declares almost no secrets, because everything a local run needs already has a harmless default committed in the repository: a local database, a mock in front of anything external, obviously fake keys for the paid services. A fresh clone boots and serves for someone with no access to the secret store at all, and anything that would reach a real provider fails authentication rather than doing something expensive. Access buys you <em>stage</em>. It isn&rsquo;t the price of entry.</p>
<h2 id="committing-references-means-scanning-for-values">Committing references means scanning for values<a class="gn-heading-anchor" href="#committing-references-means-scanning-for-values" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Committing secret references only works if you&rsquo;re sure nothing real ever slips in beside them. So the same setup carries a scanner, and mise installs it without anybody being asked to:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="p">[</span><span class="nx">tools</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">gitleaks</span> <span class="p">=</span> <span class="s2">&#34;latest&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="nx">hooks</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">postinstall</span> <span class="p">=</span> <span class="s2">&#34;git config core.hooksPath .githooks&#34;</span>
</span></span></code></pre></div><p><code>mise trust</code> is the first thing anyone runs, and the first <code>mise run</code> after it installs the toolchain, which is also what fires the postinstall hook and points git at a directory holding a pre-commit secret scan. It arrives as a side effect of the step they were taking anyway, rather than a separate instruction that people skip.</p>
<p>The hook scans staged content only, so it&rsquo;s fast, and if mise isn&rsquo;t on the PATH it skips with a loud warning instead of failing. A GUI git client&rsquo;s environment is often minimal, and blocking someone&rsquo;s commit over a PATH problem is a good way to teach them to pass <code>--no-verify</code> forever. CI scans the same commits again after the push.</p>
<h2 id="and-the-deployments">And the deployments<a class="gn-heading-anchor" href="#and-the-deployments" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>People tend to assume this bit works differently than it does.</p>
<p>Our deployed environments don&rsquo;t run fnox. The platform they run on reads the same values from the same store itself and hands them to the process as ordinary environment variables. No application code knows about any of this. Nothing in the runtime imports a library, and no build has to be told which environment it&rsquo;s for.</p>
<p>That&rsquo;s deliberate. The store is the source of truth. fnox is how a <em>human</em> reads that truth, with their own credentials, on their own machine, and the deployment reads the same truth with the machine&rsquo;s identity. The two never diverge, and the application on both sides just sees environment variables like it always did.</p>
<h2 id="what-it-doesnt-fix">What it doesn&rsquo;t fix<a class="gn-heading-anchor" href="#what-it-doesnt-fix" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>The honest version, then.</p>
<p>You still have to install mise, and put <code>mise activate</code> in your shell profile. That&rsquo;s a real setup step and I&rsquo;m not going to pretend it away. It&rsquo;s one step, it&rsquo;s the same step in all sixty-eight repositories, and it never goes stale, which is three things the paragraph it replaced could not say.</p>
<p>Docker is still yours. mise installs tools, not daemons. Our local stacks need containers, and the first-run experience for anyone without Docker configured is still a manual step and a download measured in gigabytes. Same for Xcode, which remains its own weather system.</p>
<p>Getting the tooling right removes the stupid part of a cold start. It does nothing about the hard part, which is understanding what the service actually does. That&rsquo;s still reading code, and it still takes days.</p>
<p>You&rsquo;re adding two dependencies to every developer&rsquo;s day. mise is mature and widely used. fnox is young. We&rsquo;ve committed real workflows to it, and if it stalls we&rsquo;d be doing a migration. I think that&rsquo;s a reasonable bet: the file is declarative, the values live in a store we&rsquo;d keep either way, and the fallback is a shell script that reads the same parameters. But it is a bet, and pretending otherwise would be dishonest.</p>
<p>One line in that config deserves a careful review. fnox can be told to run a command that logs you in to the store when your session has expired, which is genuinely convenient and also means a committed config file now specifies something that executes on every reviewer&rsquo;s laptop. Review that line the way you&rsquo;d review a CI workflow.</p>
<p>There&rsquo;s also a failure mode with a misleading error. Point the provider at the wrong account and what comes back is &ldquo;not found&rdquo;, which reads like a missing secret rather than a wrong identity, and sends people looking in entirely the wrong place. Both of our files carry a comment naming exactly which account the credentials have to resolve to.</p>
<h2 id="where-we-actually-are">Where we actually are<a class="gn-heading-anchor" href="#where-we-actually-are" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Two repositories fully on fnox. A handful more on mise. Sixty-odd to go, most of which will never need the secrets half at all.</p>
<p>I haven&rsquo;t put a stopwatch on it and I&rsquo;m not going to invent a figure I didn&rsquo;t measure. The shape of the answer is clear enough though. On a machine that&rsquo;s been through this once already, with mise installed and the toolchain cached and Docker running, a clone to a serving API is under a minute. Comfortably. What&rsquo;s left of the old two-to-six hours is all first-time-on-this-laptop cost: downloading a JDK, pulling images, installing mise itself. You pay that once, rather than once per repository, and that second part is the whole difference from where we started.</p>
<p>In the repositories where this landed, the setup section of the README went from a list of things to install and a paragraph about asking a colleague down to this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mise trust          <span class="c1"># once, to allow this repository&#39;s config</span>
</span></span><span class="line"><span class="cl">mise run serve
</span></span></code></pre></div><p>Two lines, and the second one is the one you wanted anyway. The first is mise asking whether you actually trust a config file that&rsquo;s about to install software and set environment variables, which, given that this file can now run commands on your machine, is a prompt I&rsquo;m glad exists.</p>
<p>The pattern generalises well past our particular stack, which is why I think it&rsquo;s worth writing down rather than filing as an internal chore.</p>
<p>Then delete the setup guide, because it&rsquo;s finally lying about how much work there is.</p>
]]></content:encoded>
      <category>Engineering</category>
      <category>Infrastructure</category>
    </item>
    <item>
      <title>Road to glory</title>
      <link>https://build.golfnext.com/en/posts/road-to-glory/</link>
      <pubDate>Thu, 13 Aug 2026 14:00:00 +0000</pubDate><dc:creator>John Allan Nielsen</dc:creator>
      <guid>https://build.golfnext.com/en/posts/road-to-glory/</guid>
      <description>At a previous job I restructured 250,000 lines of code, and a task that had taken two days started taking three minutes. Nobody got a hundred times better at programming — the restructuring deleted everything that wasn&#39;t the work. Then frontend developers started picking up backend tickets. This is the plan to do that again here, and the part of it that gets worse before it gets better.</description>
      <content:encoded><![CDATA[<p>Ask a small company where its work actually waits and the answer is almost never capacity. It&rsquo;s routing.</p>
<p>A change gets described in one sentence, everyone agrees it should happen, and then it sits — because the repository it lives in has exactly one person who can confidently open it. Not because they&rsquo;re guarding it, but because they&rsquo;re the only one who knows this project wants a JDK three majors behind the last one you used, and that what looks like the entry point stopped being the entry point in 2021.</p>
<p>We have over seventy repositories at GolfNext, built over thirteen years in half a dozen languages, and for a fair number of them that count of people is one. It&rsquo;s the subject of this post; everything else is downstream of it.</p>
<p>&ldquo;Road to glory&rdquo; is an ambitious name for a plan whose first three steps are boring. The title isn&rsquo;t ironic: the ambition is for this to become the platform the golf industry runs on, worldwide, and boring is what an engine for that is made of.</p>
<h2 id="the-number-that-actually-hurts">The number that actually hurts<a class="gn-heading-anchor" href="#the-number-that-actually-hurts" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p><a href="/en/author/jb/">Our tech lead</a> has <a href="/en/posts/four-products-68-repos/">written here</a> about the state of the estate, and I spent <a href="/en/posts/in-the-jungle/">my first two weeks</a> finding out which repositories could still start on a fresh machine. Neither account was a complaint. Over seventy repositories is what thirteen years of shipping working software to real golf clubs looks like, and the tidying is only possible because the product works. But there&rsquo;s a failure mode in it they didn&rsquo;t name directly, and it&rsquo;s the one that decides how fast this company can grow.</p>
<p>When a codebase has one maintainer, three things are true at once, and none of them are that person&rsquo;s fault.</p>
<p><strong>The work queues behind them.</strong> Everything that touches that system waits on the one calendar it can wait on. Fine at three things a quarter. Ruinous at three a week.</p>
<p><strong>The knowledge never leaves their head.</strong> Not because they&rsquo;re unwilling to share it, but because nothing takes it out. Documentation doesn&rsquo;t: people write about the parts they found interesting, which are by definition not the parts a newcomer gets stuck on.</p>
<p><strong>Nobody can check their work.</strong> A review by somebody who can&rsquo;t run the code is a review of the diff&rsquo;s spelling.</p>
<p>Put those together and you get a system technically owned by the company and practically owned by a person. We have several. So does every company this age.</p>
<p>There&rsquo;s a second-order cost I only understood by living through it: when a newcomer hits something broken, they cannot tell whether the repository is broken or they are. I lost days to a local database that turned out to be impossible to build from scratch — four migrations, in a chain of 251, that could not run against a fresh import — and for most of those days I assumed I was the problem. That&rsquo;s the natural assumption in your first fortnight, and nobody could have corrected me, because everyone already set up had a database that predated the break. Eventually I stopped assuming it was me, and fixed it migration by migration.</p>
<p>That tax lands only on new people, which is exactly why it&rsquo;s invisible to the people who could remove it. It&rsquo;s also one of the cleaner arguments for this plan: when every repository behaves the same way, a newcomer has a control group. If this one differs from the last five, it&rsquo;s the repository that&rsquo;s wrong.</p>
<p>And it scales the wrong way. We&rsquo;re in eighteen countries with more than four hundred venues and the plan involves a good deal more of both, so a bus factor of one doesn&rsquo;t stay put as you grow — the same person simply becomes the bottleneck for more things.</p>
<h2 id="what-different-costs-itemised">What &ldquo;different&rdquo; costs, itemised<a class="gn-heading-anchor" href="#what-different-costs-itemised" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>It&rsquo;s easy to say a mixed stack is expensive and hard to say where the money goes. Here is where it goes, in the order I&rsquo;ve actually felt it.</p>
<p><strong>Switching.</strong> Move between three repositories in a morning and you pay a small tax at each hop: which build tool, which version, which command, which idioms. Individually trivial. It&rsquo;s the frequency that gets you, and the frequency is a function of how many different shapes you own.</p>
<p><strong>Integrations.</strong> This is the one that matters most to our business and gets discussed the least. Our position in the market is that a club keeps its own hardware and its own management system and we work with what they already run. That means every new partner is another integration, and the cost of an integration is roughly the work itself multiplied by how strange the codebase you&rsquo;re putting it in happens to be. Write the fifth one into the same shape as the first four and it&rsquo;s a fraction of the price. Write it into a codebase with its own conventions and you pay the archaeology again.</p>
<p><strong>Onboarding.</strong> A new engineer has to learn the company once, and then learn each system separately. The second part is where the weeks go. If every system has the same shape, learning the second one is nearly free, and that&rsquo;s the difference between a new person being useful in a fortnight and being useful in a quarter.</p>
<p><strong>Review.</strong> Standardisation is what makes a real review possible for more than one person. If I can run your code, I can disagree with it.</p>
<p><strong>Agents.</strong> Our tech lead has written that the repositories where he trusts a coding agent most are, without exception, the ones somebody has already tidied. That matches my experience exactly. An agent working in a clear, consistent structure produces something you can use. An agent in an unfamiliar one guesses fluently, and you spend your afternoon working out which parts were guesses. Consistency isn&rsquo;t just nice for humans anymore. It&rsquo;s the thing that decides whether half our tooling works.</p>
<h2 id="sameness-of-shape-beats-sameness-of-language">Sameness of shape beats sameness of language<a class="gn-heading-anchor" href="#sameness-of-shape-beats-sameness-of-language" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Here&rsquo;s the part I&rsquo;d argue with anyone about, because it&rsquo;s the bit people get backwards.</p>
<p>&ldquo;Lean tech stack&rdquo; gets heard as &ldquo;fewer languages&rdquo;, and fewer languages is only about a third of the benefit. The three things that decide whether you can walk into an unfamiliar repository and be useful are, roughly in order:</p>
<ol>
<li>Is the file structure the same? Does a given kind of thing live at a predictable path, under a predictable name, in every repository?</li>
<li>Are the verbs the same? Can you build, test and run it with the commands you used in the last repository?</li>
<li>Is it the same language and framework?</li>
</ol>
<p>Number three is the one that gets the headline and it&rsquo;s the least important of the three. I&rsquo;ve moved between two Java services with completely different package structures and it was harder than moving between a Java service and a TypeScript one that were laid out identically.</p>
<p>Numbers one and two are also enormously cheaper to fix. The verbs we&rsquo;ve already largely solved: mise puts a repository&rsquo;s toolchain and its commands in a file in the repository, so <code>build</code>, <code>test</code> and <code>run</code> mean the same thing everywhere they&rsquo;ve been applied, whatever&rsquo;s underneath. That was our tech lead&rsquo;s work and it&rsquo;s the highest return-on-effort thing anybody has done here.</p>
<p>File structure is the other one, and it&rsquo;s the one I&rsquo;d put real effort into, because it&rsquo;s what you use before you&rsquo;ve read a single line. You don&rsquo;t find code by reading it. You find it by guessing a path. Where every repository agrees on where the handlers live, where the database code lives, where the tests live and what the files are called, that guess is right first time, and a search that worked in one repository keeps working in the next. Where they disagree, every repository is somebody else&rsquo;s filing system and you open four wrong files before the right one — which is most of what the first hour in an unfamiliar codebase actually consists of.</p>
<p>It&rsquo;s also the cheapest of the three to fix, because it requires no rewrite at all. It&rsquo;s an agreement, plus the patience to apply it to the file you already have open. Our tech lead did exactly that when he took the React proof of concept into the real repository: a pass on how directories and files get named, before anything got built on top of it. And it&rsquo;s the part an agent feels most, for the same reason you do — it locates code by predicting where the code should be.</p>
<p>Which is a useful thing to know, because it means most of the benefit of a lean stack is available before you&rsquo;ve consolidated a single language. It also means language consolidation should be justified on its own merits rather than borrowing the credit for the other two.</p>
<h2 id="two-and-a-half-days--three-minutes">Two and a half days → three minutes<a class="gn-heading-anchor" href="#two-and-a-half-days--three-minutes" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>So far this is an argument, and arguments are cheap. Here&rsquo;s why I believe it.</p>
<p>At a previous job I spent a long stretch restructuring about 250,000 lines of code. Not a rewrite. Same language, largely the same logic, reorganised: where things live, what is allowed to depend on what, and what shape a change has to be to fit.</p>
<p>Before it, a simple task — the ordinary recurring kind that makes up most of a week — took two to three days. Afterwards I got curious enough to time myself on one, and it came out at three minutes.</p>
<p>I should be precise about that number, because it&rsquo;s the kind of figure that gets repeated until it turns into a lie. Three minutes was one task, timed once. Others were five minutes, or ten. What changed wasn&rsquo;t that every job became a three-minute job. It was that the unit changed: work counted in days was now counted in minutes, and none of it went back.</p>
<p>Even the cautious end of that is a ratio nobody should believe about themselves, so here&rsquo;s what it actually means. Nobody got a hundred times better at programming. The work itself had always been small — a few minutes of typing, buried under everything that wasn&rsquo;t the work: finding where the change belonged, reading enough of the surrounding code to be sure, working out what else touched it, and then the part that never appears on a timesheet, which is being frightened of breaking something you don&rsquo;t understand and moving slowly on purpose because of it.</p>
<p>The restructuring didn&rsquo;t make the work faster. It deleted everything that wasn&rsquo;t the work.</p>
<p>That also tells you which tasks collapse and which don&rsquo;t. Genuinely novel work, where the hard part is deciding what the thing should do, took about as long afterwards as before; structure doesn&rsquo;t help you think. What collapses is the recurring shape — the fifth integration, another field on a form, the endpoint that looks like the eleven next to it. In an estate like ours, that&rsquo;s most of the queue.</p>
<h2 id="the-result-id-actually-point-at">The result I&rsquo;d actually point at<a class="gn-heading-anchor" href="#the-result-id-actually-point-at" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Then the part I hadn&rsquo;t predicted. Frontend developers started picking up backend tickets.</p>
<p>Nobody asked them to. When every service is laid out the same way, the barrier turns out to have been structural rather than a matter of knowledge. That&rsquo;s the most useful evidence in this post because it isn&rsquo;t self-reported: a productivity figure is something you say about yourself, and crossing a boundary you&rsquo;d avoided for years is something you do. It moved the number this post opened with, without hiring anyone.</p>
<p>One more thing, because this blog writes about coding agents constantly and it would be easy to read all of that as an AI story. There was no AI in it. None. Every minute of the gain came from where the code sat and what was allowed to depend on what — which means the payoff isn&rsquo;t contingent on agents continuing to get better, or on us using them well. Structure paid before any of this existed. What agents do is multiply it, most of all in the repositories that already have it. If they stalled tomorrow the argument in this post would be unchanged.</p>
<p>I want to reproduce that here. Different company, different estate, same mechanism. That refactor is also where this post&rsquo;s title comes from, and it&rsquo;s why I can write a plan this confident about work that hasn&rsquo;t happened yet: I&rsquo;m not predicting an effect, I&rsquo;m expecting one I&rsquo;ve already stood in the middle of.</p>
<h2 id="what-the-engine-is-for">What the engine is for<a class="gn-heading-anchor" href="#what-the-engine-is-for" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>A post about making developers more productive can be read the wrong way, so let me close that reading off myself. This is not about needing fewer developers. Nobody here is doing it so the same work can be done by fewer people, and if that were the plan I wouldn&rsquo;t be writing a blog post about it. The developers here are good. They built a product that works in eighteen countries, under constraints I never had to live with, and losing any of them would set this back rather than forward.</p>
<p>The reason is growth, and I&rsquo;d rather state the size of it than be coy. We are not trying to get somewhat bigger. The goal is for this to be the platform the golf industry runs on — every club, every range, every country that has both — and the reason that&rsquo;s sayable out loud rather than embarrassing is that the demand side is already turning up. Clubs who see the product are, in my experience so far, genuinely amazed by it. That reaction is the asset. Nothing in this post creates it, and nothing in this post can substitute for it.</p>
<p>What we don&rsquo;t have is the engine to serve it at that size. The product is good; the machinery that builds, integrates and supports it is thirteen years of accumulated decisions, and that&rsquo;s the part that breaks first if demand arrives faster than the team can absorb it. We&rsquo;re not building the product — it exists, and people who had nothing to do with building it are impressed by it. We&rsquo;re building the thing underneath that decides how many clubs, how many countries and how many integrations a small team can carry at once. Amazed customers are what got us to more than four hundred venues in eighteen countries. What decides whether the number after that has another zero on it is whether the eleventh integration costs what the first one did.</p>
<p>Which is why the part of this I care most about getting right isn&rsquo;t the language. It&rsquo;s that the people who&rsquo;ll have to live in the result want to.</p>
<h2 id="react-on-the-front-go-behind-it">React on the front, Go behind it<a class="gn-heading-anchor" href="#react-on-the-front-go-behind-it" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>What you&rsquo;re looking for when you finally do pick a language, then, isn&rsquo;t the most powerful one. It&rsquo;s the one that hands you the other two for free.</p>
<p>The target is two languages.</p>
<p>Every interface is React. Web is React and TypeScript, mobile is React Native, and that includes the interfaces sitting on hardware — the kiosk screen a player stands in front of at half five in the morning is a screen like any other. That was already half-true before anyone decided it, because our mobile applications are React Native, and it became true in practice when one of our frontends went from Vue to React and shipped. The rest follow.</p>
<p>Everything behind those interfaces is Go. Not &ldquo;the new services&rdquo; — everything. The web backends, the public API, the integrations, and the desktop point-of-sale application, which becomes a Go core with a React interface instead of the JavaFX application it is today. Whether a thing runs in a data centre or on a till in a pro shop stops being a fact about what language it&rsquo;s written in.</p>
<p>I&rsquo;m not going to argue the languages here. Go against the alternatives is a post of its own, and the honest summary is that the specific choice matters considerably less than the fact that there is one — which is the argument this entire piece has been making. Worth being plain about one thing before the next section, though: Go is not what we have most of. Java, PHP, Node and Kotlin all have more running code behind them today, and that&rsquo;s a real cost rather than a detail.</p>
<p>What the decision buys is two things, and they&rsquo;re worth separating because they get blurred together in every post like this one. The first is developer experience, which is most of what this post is about and which comes down to the number at the top: how many people can open a given repository and be useful in it.</p>
<p>The second is performance, and there I want to be precise rather than sloganeering, because &ldquo;simple stack, therefore fast&rdquo; is not an argument. What it does mean is two specific things. One is footprint: what a process costs to start and to keep running, which is a real number on kiosk hardware and a real line on a cloud bill. The other is that performance work is gated on comprehension — you cannot fix the slow thing in a system nobody can hold in their head, and most systems are slow in a place their maintainers stopped looking at years ago.</p>
<p>One sub-decision genuinely isn&rsquo;t settled. A Go core with a React interface on the desktop needs a shell to host that interface, and we haven&rsquo;t picked one. The shape is decided; the mechanism isn&rsquo;t, and it isn&rsquo;t urgent, because the desktop application is nowhere near the front of the queue.</p>
<h2 id="the-shot-were-not-taking">The shot we&rsquo;re not taking<a class="gn-heading-anchor" href="#the-shot-were-not-taking" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>On most courses there&rsquo;s a hole where the ambitious line goes straight over the water. The big-bang rewrite is that shot: freeze the old thing, build the new one beside it, switch. Quickest if it comes off — and the graveyard is full of companies that spent two years shipping nothing while their competitors shipped features.</p>
<p>We&rsquo;re laying up. Four rules.</p>
<p><strong>New work is built in the target shape.</strong> The only free rule, and the one that does most of the work over a year: every new service is a repository that never needs converting.</p>
<p><strong>Rewrite only what you were opening anyway.</strong> Conversion is cheapest when somebody is already paying to understand the system. Working software nobody needs to change isn&rsquo;t a problem; it&rsquo;s an asset with an ugly logo.</p>
<p><strong>Seams before rewrites.</strong> Two admin portals, two backends, two logins. Not a rewrite into one: an auth bridge first, then a single frontend over both backends, then the backend consolidation. Each step separately shippable, and separately abandonable.</p>
<p><strong>Structure and verbs first, everywhere; language last, selectively.</strong> The cheap two-thirds is available in every repository today, including the ones that will never be converted.</p>
<p>Then the thing I&rsquo;d rather write down than be caught not having thought about: it gets more varied before it gets less. Go isn&rsquo;t a language we have much of, so for a couple of years the estate holds Java, PHP, Node, Kotlin <em>and</em> Go. Convergence arrives last. If you want to argue with this plan, argue there — it&rsquo;s the weakest point in it, and the only defence is that the four rules exist to make that interval survivable.</p>
<p>One caution rather than a rule: agents have made rewrites cheap to produce and no cheaper to review. A rewrite that looks finished is the most flattering thing they make, because its gaps are omissions, and an omission never shows up in a demo.</p>
<h2 id="what-this-actually-costs">What this actually costs<a class="gn-heading-anchor" href="#what-this-actually-costs" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>The plan has a bill and I&rsquo;d rather itemise it than have somebody else do it.</p>
<p><strong>Monoculture is a real risk, not a rhetorical one.</strong> Two languages everywhere means two sets of blind spots, two dependency ecosystems whose bad weeks are now everybody&rsquo;s bad week, and a team that gradually loses the reflex of asking whether the usual tool is the right one.</p>
<p>The strongest form of that objection is the right tool for the job, and in general I agree with it. But it&rsquo;s an argument about problem spaces, and ours is one problem space. Take the labels off our systems and they are all doing recognisably the same work: accept a transaction, talk to a device or to a partner&rsquo;s API, and put a record somewhere it can be trusted later. There&rsquo;s no machine-learning workload here pulling towards Python and no signal processing pulling towards C. The variety in our estate corresponds to no variety at all in the problems it solves, which is the tell that it&rsquo;s accident rather than design. Nobody picked half a dozen languages as a hedging strategy.</p>
<p>So the risk is real and worth naming, and the mitigation is that &ldquo;standard&rdquo; has to mean a strong default with a documented way to argue, not a rule. The day a genuinely different problem does turn up — and in a company doing more with AI every quarter, it might — that argument has to be winnable, or the default has quietly become a rule after all.</p>
<p><strong>Consolidation can be a way of avoiding product work.</strong> Structural work is satisfying and legible and it produces no customer value on the day it lands. The discipline is that every step has to be justified by something it unblocks. The portal merge is justified because club staff log in twice. The React standardisation is justified because it&rsquo;s the precondition for everything after it. If a step can&rsquo;t name what it unblocks, it isn&rsquo;t ready.</p>
<p><strong>It replaces work people are proud of.</strong> This is the one management raised when we presented the plan, ahead of cost and ahead of customer risk, and they were right to. Nobody wants to hear that the thing they built well, under the constraints they actually had, is now the old way. We ran the brainstorm with the whole team and deliberately held our own proposal back until everyone else had spoken. Nobody disagreed. The parts about merging the portals people were genuinely excited about; the standardisation itself people were fine with, which is a reasonable thing to feel when it&rsquo;s your choice being replaced, and I&rsquo;d rather report that accurately than upgrade it.</p>
<p><strong>And it will take longer than this post makes it sound.</strong> Golf clubs are busy in summer, so we&rsquo;re busy in summer. The platform goes quiet over the winter and that&rsquo;s when the structural work has room. This is a plan measured in seasons.</p>
<h2 id="how-well-know-it-worked">How we&rsquo;ll know it worked<a class="gn-heading-anchor" href="#how-well-know-it-worked" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Not by a diagram. By four things you can actually check.</p>
<p>How many repositories have exactly one person who can confidently ship a change to them. Today that number is uncomfortable. It&rsquo;s the headline metric for this entire plan and it&rsquo;s the only one I&rsquo;d defend as being about the company rather than about engineering taste.</p>
<p>How long from clone to running code, on a machine that&rsquo;s been through it once. That was two to six hours when our tech lead measured it in August, depending which repository you picked. In the repositories mise and fnox have reached, it&rsquo;s a command.</p>
<p>The ambition behind that number is blunter than the number itself, so I&rsquo;ll put it the way we actually say it: getting a codebase running locally should be as close to idiot-proof as we can make it. Not because anyone here is an idiot — the opposite. If starting a repository takes cleverness, that cleverness is being spent on the wrong thing, and it gets spent again by every person who arrives after you. Clone it, run one command, get a working system with a database, test data and an account to log in as. Anything standing between those two points is a defect, whether or not it&rsquo;s in a tracker.</p>
<p>What the second integration costs relative to the first. If we do the structural work and the fifth partner integration still costs what the first one did, the work didn&rsquo;t do what we said it would.</p>
<p>And what a new engineer can do at the end of their second week. Not &ldquo;has read the codebase&rdquo;. Has shipped something to a club.</p>
<p>That last one is the whole plan compressed into a sentence, and it&rsquo;s the reason for all of it. Everything in this post — React on the screens, Go behind them, the folder layouts, the auth bridge, the one command that means the same thing in every repository — exists so that the answer to &ldquo;can you pick this up?&rdquo; stops depending on which repository we&rsquo;re talking about.</p>
<p>And the reason to care about a question that small is that its answer caps everything above it. A club somewhere decides we&rsquo;re the best thing they&rsquo;ve seen this year, and from that moment the only question that matters is whether we can build what they need while they&rsquo;re still excited about it. Doing that four hundred times is a product. Doing it four thousand times is an engine.</p>
<p>So here&rsquo;s the version I actually believe, written down where you can hold me to it.</p>
<p>Two years from now, a developer who joined last month picks up an integration with a management system in a country we don&rsquo;t sell in yet. They have never opened that repository. They clone it, <code>mise run serve</code> fetches the toolchain and brings the thing up, the Go handlers are where Go handlers always are, the React screens are laid out like every other React screen we own, and the tests pass first time. By Thursday it&rsquo;s live at a club that had never heard of us in the spring. Nobody in the building thinks this is remarkable.</p>
<p>That&rsquo;s the whole ambition. Not a heroic quarter — a Thursday.</p>
<p>Multiply that Thursday by every developer we have and every country that has golf in it, and you get a company that ships as fast as it can think, in an industry that has never once had software moving at that speed. The product that amazes people already exists; we didn&rsquo;t have to invent it and I can&rsquo;t take credit for it. What we&rsquo;re building now is the only thing standing between that product and every club on earth that would want it — and we&rsquo;d rather build it before somebody else works out that this is the race.</p>
<p>So: glory is a big word, and it&rsquo;s the right one. A course record isn&rsquo;t one miraculous shot — it&rsquo;s eighteen ordinary holes played by somebody who has stopped losing strokes to the rough. We&rsquo;re not lining up the hero shot over the water. We&rsquo;re removing every place the ball can get lost, and then we&rsquo;re going to play the round of our lives.</p>
<p>Which in practice is as unglamorous as it sounds, and I&rsquo;d rather finish there than on the metaphor. React on every screen and Go behind every one of them. The same folder structure in every repository, so you can guess a path before you open the tree. One command that builds any of them. Secrets in a store instead of a chat message. Over seventy repositories converging on a single shape — and a company that can then take on the hundredth without anybody having to be brave about it.</p>
]]></content:encoded>
      <category>Architecture</category>
      <category>Engineering</category>
    </item>
    <item>
      <title>In the jungle</title>
      <link>https://build.golfnext.com/en/posts/in-the-jungle/</link>
      <pubDate>Thu, 13 Aug 2026 13:00:00 +0000</pubDate><dc:creator>John Allan Nielsen</dc:creator>
      <guid>https://build.golfnext.com/en/posts/in-the-jungle/</guid>
      <description>Part of my onboarding was a task with my name on it: get every codebase running locally. Seventy-one repositories, a decade of technology choices, and two weeks of finding out which of them could still start on a fresh machine — including the one whose local database could not be built at all, and what it took to make one command build it anyway.</description>
      <content:encoded><![CDATA[<p>I started at GolfNext at the beginning of August. Somewhere in the onboarding, between the accounts and the hardware, was a task with my name on it: get every codebase running locally.</p>
<p>Not read them. Run them. Clone the repository, install whatever it wants, start the thing, and watch it come up on your own machine.</p>
<p>It reads like something you hand a new person to keep them busy for a day. It ran through most of my first two weeks, it produced the most useful map of this company I have, and it turned into a fair amount of repair work nobody had asked for, because several of those repositories could not start at all.</p>
<p>In my first week alone I worked in a Vue admin panel, an Android app that runs on a self-service kiosk, a Java backend, and a desktop point-of-sale application that ships as a Windows executable.</p>
<p>That&rsquo;s the shape of the company. GolfNext builds the software golf clubs run on — booking and paying for a round, the till in the pro shop, the unattended machines out on the practice range — and after more than a decade of doing it there are seventy-one repositories in our GitHub organisation. Mostly TypeScript, JavaScript and Java, with PHP behind them, and then a few one-offs: Kotlin, HCL for the infrastructure, shell.</p>
<p>None of that is unusual for a company this age, and none of it is a complaint. It exists because people shipped a decade of working product under real constraints, and the tidying up I&rsquo;m about to describe is only possible because the product already works. It does mean every repository is a small patch of jungle, though. Two things were genuinely hard, and neither of them was the actual programming: working out which of seventy-one repositories the thing I needed even lived in, and then working out how to start it. The second one was the assignment.</p>
<h2 id="what-the-jungle-charges">What the jungle charges<a class="gn-heading-anchor" href="#what-the-jungle-charges" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Big obstacles are interesting, and you remember them. The jungle charges you in small identical amounts instead, over and over.</p>
<p>You clone something and the build fails because your JDK is one major version off. You fix that and the build tool wants a different one. You move to a web project and the version file says one Node while the lockfile was built against another. Somewhere there&rsquo;s an environment file to fill in, and the values live with a colleague. Each of those is fifteen or twenty minutes, none of them teaches you anything, and you pay them again on the next machine and again for the next person who joins.</p>
<p>None of that was any one repository&rsquo;s fault. It was the absence of a shared answer: every Java application had settled on its own JVM version, and every web project on its own Node version, each pinned in practice by whatever happened to be current the last time somebody set it up, with nothing ever going back to reconcile them. The dependencies underneath drifted the same way — the same libraries sitting at different versions in different repositories — so what you learned getting one repository to build didn&rsquo;t transfer to the next one.</p>
<p>And getting any of them to run locally was its own tax, payable in a different currency each time. The clearest case: two of our Java desktop applications are the same kind of program, built for the same job — one builds with Gradle, the other with Maven. Neither repository records a reason. At some point I stopped taking notes and asked the question out loud, which is the question this whole post is really about: where is the standardisation? Not as a complaint. As a genuine question about how an estate ends up holding two answers to a problem that only has one.</p>
<h2 id="one-person-one-application">One person, one application<a class="gn-heading-anchor" href="#one-person-one-application" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>There&rsquo;s a structural reason for this, and I think it explains most of what I found. For most of this company&rsquo;s life the software was built by remote developers, working at a distance from other countries and, as far as I can tell, largely on their own: one person, one application, start to finish. If you are the only one who will ever build a thing, declaring its toolchain in the repository buys you nothing. You already have the toolchain — it&rsquo;s on your machine, and it&rsquo;s correct because it&rsquo;s yours. Conventions converge when people have to work inside each other&rsquo;s repositories, and for years almost nobody did.</p>
<p>I&rsquo;d add a softer theory, and I want to be clear that it is one. Distance isn&rsquo;t only geographic. Working across countries, time zones and second languages makes the cheap conversations expensive — the ones where two developers notice in passing that they&rsquo;ve solved the same problem differently and settle on one way of doing it. Nobody has to disagree for an estate to diverge. It&rsquo;s enough that the conversation is never quite convenient. None of which is a criticism of anyone who worked here before me; it&rsquo;s roughly what I&rsquo;d expect that arrangement to produce, and I&rsquo;d probably have built the same jungle in their position.</p>
<p>All of that has a name, and it&rsquo;s the oldest joke in the industry: works on my machine. Nobody here says it with a shrug, and nobody was defending it — but it&rsquo;s what the estate had quietly organised itself around. If every machine that builds a given repository is already configured, then the configuration doesn&rsquo;t live in the repository. It lives in a few people&rsquo;s heads and in their shell history. Onboarding a new person means reading it back out of them, and it is never complete, because the parts they do automatically are exactly the parts they forget to mention.</p>
<h2 id="the-repository-that-cannot-start">The repository that cannot start<a class="gn-heading-anchor" href="#the-repository-that-cannot-start" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>The worse case is the repository that cannot start at all.</p>
<p>In one of our backends the local development profile — the configuration that points the application at a database on your own machine — had been deleted months earlier, and nobody noticed, because nobody who was already set up ever needed it. Clone it fresh and you get a data-source error that tells you nothing about why. Underneath that sat something worse: a database migration written years ago that won&rsquo;t let the schema finish until it has called out to a third-party service, one this codebase can no longer reach. So a genuinely fresh local database could not be built at all, on any profile.</p>
<p>Neither of those was in a tracker. They aren&rsquo;t bugs in the ordinary sense, and nobody did anything wrong to create them. They&rsquo;re what a repository drifts into when no fresh machine is ever the one being tested — and you only find out on the day somebody joins, or somebody&rsquo;s laptop is replaced, when the whole bill arrives at once.</p>
<h2 id="two-hundred-and-fifty-one-migrations">Two hundred and fifty-one migrations<a class="gn-heading-anchor" href="#two-hundred-and-fifty-one-migrations" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>The hardest one to start was our GolfMore admin panel — the back office club staff log into, which serves the public API out of the same codebase. PHP and Symfony, MySQL underneath, and a decade of schema history sitting on disk: a 114 KB base dump to import first, then 251 migration files to run on top of it.</p>
<p>The documented way to get a local database was exactly that. Import the dump, run the migration chain, get a working schema. It does not work, and hasn&rsquo;t for years, because four migrations from 2022 and 2023 cannot run against a database built that way:</p>
<ul>
<li>One drops the bookkeeping table left over from the migration tool the project used before Doctrine, which a fresh dump import has never had.</li>
<li>One throws unconditionally in its <code>postUp()</code>, because it calls <code>addSql()</code> on a migration that is already frozen.</li>
<li>One references a table several months before the migration that creates it.</li>
<li>One declares <code>AUTO_INCREMENT</code> on a column with no key, which MySQL refuses, so it has never been able to run at all.</li>
</ul>
<p>Nobody is at fault here in an interesting way. The combined effect is the problem: the one path a new developer is told to take is the one path that&rsquo;s broken, and everybody already set up has a database that predates it, so nobody else ever hits it.</p>
<h2 id="how-a-chain-rots">How a chain rots<a class="gn-heading-anchor" href="#how-a-chain-rots" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>How it got this way I can only guess at, and I&rsquo;d rather say that than pretend otherwise. My best guess is that at some point the schema in production was changed by hand. The fourth of those defects is nearly proof of it: MySQL has never accepted an auto-increment column that isn&rsquo;t a key, so that migration cannot have run anywhere, ever — and yet the table it describes exists everywhere the application runs. Something other than the migration chain put it there. The same explanation covers the forward reference: if that table already existed on the machine where the migration was written, referencing it worked fine there, and only a database built strictly from the chain would ever notice.</p>
<p>Two other mechanisms need no villain at all. One is the base dump. It&rsquo;s a snapshot, and if it was refreshed at some point later than the migrations that follow it, then importing it and replaying the chain stops being the history that actually happened — you&rsquo;re now asking old migrations to redo what the dump already contains, or to drop what it never had, which is exactly the leftover bookkeeping table. The other is dependency upgrades. The migration that throws in its post-run hook was presumably legal the day it was written, and a later version of the migrations library made that call illegal. Nobody re-runs the old end of a chain, so a change like that lands silently and waits for whoever builds a database next.</p>
<p>Which is the general shape of the thing. A migration chain only ever gets tested at its head. The tail is exercised by exactly one kind of event — somebody building an environment from nothing — and if that hasn&rsquo;t happened for a few years, nothing anywhere tells you it has rotted.</p>
<h2 id="was-it-me-or-the-code">Was it me or the code?<a class="gn-heading-anchor" href="#was-it-me-or-the-code" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>What made this slow wasn&rsquo;t any one of those four. It was not knowing, for a good while, whether the problem was the code or me. A new person with a failing command has no way to tell a broken repository from their own missing knowledge, and the default assumption — the polite one, and usually the correct one — is that it&rsquo;s you. So you re-read the README, check your versions, throw it away and start from a clean clone, and lose an afternoon proving something that was never true in the first place.</p>
<p>Once I accepted it was the chain, the method was dull and it worked. Run the migrations up to the version where they stop. Work out what that one needed and couldn&rsquo;t get. Write the patch by hand, mark that version as done without running it, and carry on to the next dead end. I did that a handful of times, each round a little faster than the last, until the whole chain ran end to end without breaking anything.</p>
<p>Then I wrapped the resulting list of hacks in a single mise task, which is the only part anybody else should ever have to see — and the reason starting this thing locally now feels like a breeze instead of an archaeology project.</p>
<h2 id="what-one-command-hides">What one command hides<a class="gn-heading-anchor" href="#what-one-command-hides" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>What I did about it is not elegant. One task, <code>mise run db</code>, starts the container and then asks the database a single question: does the migrations table exist? Everything after that hangs off the answer.</p>
<p>If the table is there, this is an existing database: migrate it forward, assert it ended up current, done. If it isn&rsquo;t, the task imports the dump and walks the chain in stages, stopping at each broken migration to do by hand the one thing that migration can&rsquo;t do for itself. Create the leftover bookkeeping table so the migration that drops it succeeds. Add the column the throwing migration would have added, then mark that migration applied without running it. Stand up a stub of the table that doesn&rsquo;t exist yet, just long enough for the forward reference to resolve, and drop it again afterwards. Create outright the one table whose migration can never execute. Then run the remainder normally, verify the schema is up to date, and print the test login.</p>
<p>I&rsquo;d rather have fixed the four migrations. That&rsquo;s a bigger job than it looks, because they&rsquo;re part of a chain every deployed environment has already run, so touching them means being certain what each of those environments actually has. So the workaround is what shipped — with the root causes written up, and a note above the block saying the whole thing can be deleted once they&rsquo;re fixed. Ugly and documented beats elegant and imaginary. The failure mode I care about is the next person spending two days rediscovering all four.</p>
<p>The result is the part I&rsquo;d defend. On a machine with nothing but mise and Docker, one command brings up the panel, the API, a schema at the right version, and an account to log in as. Re-running it is safe. And <code>mise run db:reset</code> throws the database away, so you can prove the whole thing works from empty — which is the real test, and the one nobody had been in a position to run.</p>
<h2 id="what-the-count-multiplies">What the count multiplies<a class="gn-heading-anchor" href="#what-the-count-multiplies" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Two costs don&rsquo;t just repeat with the repository count. They scale with it.</p>
<p>The first is dependency bumps. In that same week I bumped an HTTP client by hand in two frontends, a JSON library in a third repository and a PDF library in a fourth: four branches, four pull requests, four review cycles, and nothing learned by anybody. The usual case for automating that away is security. At seventy-one repositories it&rsquo;s arithmetic.</p>
<p>The second is branch topology. Some repositories still carry a branch each for development, staging and production, with changes travelling between them by merge. From that week I have a commit of my own whose entire message is &ldquo;Fixed merge conflict error&rdquo;. Every long-lived branch is another place the same change exists in a slightly different state. We&rsquo;re moving to one, where a merge deploys to staging and a published release deploys to production, and nothing is left to reconcile.</p>
<h2 id="the-machete">The machete<a class="gn-heading-anchor" href="#the-machete" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Two tools do most of the cutting, and neither of them was my idea — <a href="/en/author/jb/">our tech lead</a> brought both in during <a href="/en/posts/four-products-68-repos/">his own first weeks</a>. <a href="https://mise.jdx.dev/">mise</a> declares a repository&rsquo;s toolchain — and the commands you run against it — in a file in the repository, and <a href="https://fnox.jdx.dev/">fnox</a> does the same for the <em>references</em> to its secrets. The repository commits pointers; the values live in a managed store that grants access per person, rather than in a file somebody pastes you.</p>
<p>The Android app got its Java and Gradle versions pinned. The backends got Java and Maven pinned. Several repositories got a README that was true again, and a CI workflow where there hadn&rsquo;t been one.</p>
<p>The one I liked most was the desktop application. It ships as a Windows executable, and it now cross-builds from Linux through a task defined in the same file that pins its toolchain — hiding a toolchain I&rsquo;d otherwise have to learn before touching a one-line change.</p>
<p>To be honest about the coverage: that&rsquo;s a handful of repositories out of seventy-one. It&rsquo;s less a programme than a habit a few of us keep picking up.</p>
<h2 id="what-the-machete-doesnt-cut">What the machete doesn&rsquo;t cut<a class="gn-heading-anchor" href="#what-the-machete-doesnt-cut" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>All of that shortens the distance from clone to running. It does nothing whatever to the distance from running to understood.</p>
<p>My most recent feature is a super-admin screen — the one our own staff use to manage the internal accounts that get into the back office. List them, create one, and since this week, edit one. A form, a table, a couple of calls.</p>
<p>Nothing in the repository documented what those calls expected. The contract did exist, as a published API document sitting outside the codebase, so the types came out of that rather than out of guesswork. A slightly undignified way to obtain a contract, and a much better one than reading the existing client code and hoping whoever wrote it had got it right.</p>
<p>Then the document contradicted itself. One endpoint is described as returning every account. The response it declares is a single account. Both statements sit in the same file, and I couldn&rsquo;t settle which was true by calling the thing, because I didn&rsquo;t have credentials for that environment.</p>
<p>So the client code I wrote accepts a list, or a single object, or an empty body, and always hands back an array. Three tests, one per shape, and one manual pass through the screen itself. The alternative was to pick the shape I thought most likely and fail loudly on the other, which would have converted a documentation ambiguity into an outage. The shape of that code is an ambiguity I couldn&rsquo;t resolve, and the only way anyone will know that later is that I wrote it down beside it.</p>
<p>The edit form produced three more. A password field the API doesn&rsquo;t return as empty, so the form has to open blank and send nothing unless someone types a new one — round-tripping whatever came back would write nonsense over a perfectly good credential. Roles matched on their stored identifier, so a change has to carry that identifier; send the new role without it and the account quietly ends up with two. And a save that treats a row left out of the payload as unchanged rather than deleted, so there&rsquo;s no way to remove one through it at all, which is why that delete control is disabled instead of sitting there pretending to work.</p>
<p>Each of those is one comment at the site, explaining why the code is the shape it is.</p>
<h2 id="where-the-agents-fit">Where the agents fit<a class="gn-heading-anchor" href="#where-the-agents-fit" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>I&rsquo;m generally enthusiastic about coding agents, so here&rsquo;s another commit from that same week. Its entire message is &ldquo;revering claude slop&rdquo; — my typo, my commit, reverting what an agent had produced.</p>
<p>That commit doesn&rsquo;t dent the enthusiasm so much as locate it. An agent is only as good as the ground it stands on. Point one at a repository with a clear structure and a pinned toolchain and tests that actually run, and the output is usually something you can work with. Point one at a repository where none of that is true and it will guess, fluently and confidently, and you&rsquo;ll spend your afternoon working out which parts were guesses.</p>
<p>Which means the tooling work and the AI work aren&rsquo;t two projects. Every pinned version and every honest README raises the floor under everything an agent does in that repository afterwards, and the repositories where I trust an agent most are, without exception, the ones somebody has already tidied.</p>
<h2 id="two-distances">Two distances<a class="gn-heading-anchor" href="#two-distances" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>The distance from clone to running is a tooling problem, and it compresses beautifully. It goes from an afternoon to one command that&rsquo;s spelled the same everywhere, and it compresses again for the next person.</p>
<p>The distance from running to understood does not compress. No config file will tell you that an endpoint&rsquo;s summary and its schema disagree, or that a migration from years ago is holding your database hostage over a service it can&rsquo;t reach. What helps there is slower and less satisfying: read the API&rsquo;s own description of itself rather than the code somebody wrote against it, and when you end up writing defensive code, write down what you were defending against.</p>
<p>That second distance is most of the job. The value of streamlined tooling isn&rsquo;t that it makes the job smaller. It&rsquo;s that you arrive at the real work with your twenty minutes and your patience intact — and the way in is starting to be the same in every repository.</p>
<p>As for the task: everything I pointed myself at did eventually come up, and several of them only after being repaired first. That&rsquo;s the part I hadn&rsquo;t expected from an onboarding chore. It looks like it exists to teach the new person the estate, and it does do that. What it actually produces is a list of everything that had quietly stopped working while nobody was looking.</p>
<p>Which is, as I understand it, the job. Johann, our tech lead, and I arrived within a day of each other at a company whose product works and whose technical foundation grew one application at a time, because that&rsquo;s what the arrangement that built it asked for. Bringing structure to that, and making it scale past the size we are now, is what the two of us are here to do — not as a cleanup project with an end date, but as the standing work. Everything above is week one of it.</p>
<p>And the ambition behind it isn&rsquo;t tidiness. We want to build the engine that can carry this product worldwide — the software golf clubs run on, in as many countries as will have us — and the only thing that ever earns a position like that is customers who are amazed by what they get. Structure and scale are how you stay able to deliver that when there are ten times as many of them. A foundation that can&rsquo;t scale doesn&rsquo;t fail loudly. It just quietly caps how good the product is allowed to become.</p>
]]></content:encoded>
      <category>Engineering</category>
      <category>Infrastructure</category>
    </item>
    <item>
      <title>Four products, sixty-eight repositories</title>
      <link>https://build.golfnext.com/en/posts/four-products-68-repos/</link>
      <pubDate>Wed, 12 Aug 2026 00:00:00 +0000</pubDate><dc:creator>Johann Böhler</dc:creator>
      <guid>https://build.golfnext.com/en/posts/four-products-68-repos/</guid>
      <description>Two weeks inside GolfNext. What we actually build, what thirteen years of software looks like underneath it, the integrations we started in week two, and an honest look at the goals I set myself before I&#39;d signed anything.</description>
      <content:encoded><![CDATA[<p>I started at GolfNext on Monday the 3rd of August, at 8:00. I&rsquo;d pictured a quiet office and twenty minutes of standing around. Most of the team was already in and in a good mood about it. I&rsquo;d brought a cheesecake, which we ate after lunch, because I&rsquo;ve been in Denmark long enough to know that turning up with cake is not really optional here.</p>
<p>Peter, who started the company in 2012, had Claude open before I&rsquo;d found my desk, working through the accounts and permissions I&rsquo;d need. Small thing to notice on a first morning. It told me more than an onboarding deck would have.</p>
<p>The first post on this blog was a letter I wrote to myself before I&rsquo;d signed anything, full of predictions about a company I&rsquo;d visited once. This is where I hold it up against what&rsquo;s actually here. Most of what follows isn&rsquo;t really about me though. It&rsquo;s the company I want to describe: what we build, what sits underneath it after thirteen years, and what we&rsquo;re planning to do about that.</p>
<h2 id="what-golfnext-builds">What GolfNext builds<a class="gn-heading-anchor" href="#what-golfnext-builds" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>If you&rsquo;ve never thought about the software a golf club runs on, here&rsquo;s the shape of it. A club is a small business wearing several hats at once. There&rsquo;s a pro shop, a driving range, memberships, a booking system, and more and more often a simulator bay or two. The booking system handles tee times, which, since I had to learn this myself, have nothing to do with tea. A tee time is the slot a group is booked to start their round on the first hole, and the whole day at a club is arranged around that grid.</p>
<p>Historically each of those hats arrived as its own box, from its own vendor, with its own software attached. Buy the dispenser, get the dispenser company&rsquo;s system, whether or not it suits how you run your club.</p>
<p>Our position is the opposite of that. Your hardware and your infrastructure stay yours, and you stay free to pick the software that fits. We want to be open to any integration, so whatever management system a club already uses to run its tee times, the aim is to work with it rather than ask them to throw it out. Freedom to choose, power to connect.</p>
<p><strong>Self-service kiosks.</strong> A digital reception that runs around the clock. Green fees, rentals, pro shop purchases, all without someone standing behind a counter. As far as we know it&rsquo;s the only one of its kind in golf. A club gets fewer staffed hours and keeps earning outside opening times. A player gets the same convenience they already have everywhere else in their life.</p>
<p><strong>Driving range payments.</strong> Payments on the range that work with any ball dispenser brand. That compatibility is the product. The dispenser belongs to the club, or to whoever sold it to them, and being able to sit on top of all of them is the whole selling point. Cards, apps, wallets, PIN codes, with real-time analytics behind it. In a lot of places the range still runs on cash, and this is where demand is growing fastest right now. More than 500 million balls a year go through it. That&rsquo;s a lot of slices.</p>
<p><strong>Voucher management.</strong> Issuing, distribution and redemption, all digital, from a single ticket up to a full sponsor package.</p>
<p><strong>Simulator management.</strong> Automated access control, so a simulator only runs during a paid session. No more time theft, better utilisation.</p>
<p>Where that adds up to today: 400+ venues, 18 countries, 350,000+ active users. Founded in Denmark in 2012 and still very Danish, which is also why I walked out of my first day having decided to get serious about my Danish.</p>
<h2 id="the-stack-without-the-polish">The stack, without the polish<a class="gn-heading-anchor" href="#the-stack-without-the-polish" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Sixty-eight repositories. JavaFX on the desktop side, PHP, Vue, React, React Native, Spring, Java, Kotlin. All of it on AWS.</p>
<p>Written out like that it looks like indecision, and it isn&rsquo;t. Thirteen years, four product lines, hardware sitting in the field, and for most of that history the software was built by external partners who were good at their jobs. Every project picked what made sense on the day it started, and most of those calls were reasonable ones. What never happened is anybody going back afterwards to make the choices agree with each other.</p>
<p>So, the honest state, because I said in the letter I&rsquo;d write these things down rather than smooth them over.</p>
<p>A good share of those sixty-eight repositories hadn&rsquo;t been touched in years and were never archived. Forks, spikes, one-off tools that outlived their reason. I started archiving in the first week, slowly, because for most of them I didn&rsquo;t yet know what I was looking at, and not knowing is a much better reason to leave something alone than to delete it.</p>
<p>In the code you can read the generations. Several package structures inside the same language, none of them wrong, all of them different. An <code>AGENTS.md</code> written for Codex and a <code>CLAUDE.md</code> written for Claude, sitting in the same repository, describing the same project, disagreeing about it. A <code>README.md</code> documenting a version of the system that doesn&rsquo;t exist anymore. A runtime that went end of life a while back and is still running. And a release process that&rsquo;s properly good for some of our tools and a snowflake for others, where part of it lives in a pipeline and part of it lives in somebody&rsquo;s head.</p>
<p>Security sits at the top of the pile. Of the twenty-nine engineering practice items I&rsquo;ve written down so far, five are marked high priority, and all five are security. That&rsquo;s not a discovery about GolfNext in particular. It&rsquo;s what thirteen years of shipping tends to look like anywhere nobody has recently been paid to go and look.</p>
<p>Since I made a prediction about all this from the outside, I owe you the result. It&rsquo;s roughly where I expected it to be, and I mean that as a fair outcome rather than a complaint. This is thirteen years of continuous delivery by people shipping features that customers actually needed, on deadlines, with the priorities they were handed at the time. You&rsquo;d find much the same picture at most companies this age, and I&rsquo;d rather be the person who tidies it than the person who tuts at it. The one prediction that landed exactly was the dull one: I&rsquo;d guessed from outside that the repos would be under-automated, and they are. That&rsquo;s my opening.</p>
<h2 id="the-part-i-got-wrong">The part I got wrong<a class="gn-heading-anchor" href="#the-part-i-got-wrong" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>I&rsquo;d braced myself for the products to be the weak spot. Companies carrying a stack like ours often have shipped-anyway software on top, the kind where you can feel the deadline in the interface.</p>
<p>That&rsquo;s not what&rsquo;s here. The products are good. There&rsquo;s room in all of them, obviously. But the thing I keep noticing in meetings is that everybody reasons from the customer&rsquo;s side first, including people with no engineering background at all. What does the player standing at the kiosk at half five in the morning actually experience. What does a club manager have to explain to a member when something goes wrong.</p>
<p>That instinct is hard to retrofit into a team. Package structures can be refactored and READMEs can be rewritten, and both of those are just work. Teaching a group of people to care about the person at the other end is a different order of problem, and we don&rsquo;t have it.</p>
<h2 id="what-we-started-in-week-two">What we started in week two<a class="gn-heading-anchor" href="#what-we-started-in-week-two" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Two integrations landed almost immediately, and they&rsquo;re the same bet in different clothes. The platform is only open if it actually connects to the systems clubs already run.</p>
<p><strong>teeControl</strong> is a management system a lot of clubs use, and we&rsquo;re integrating it so it works through our kiosks. First impressions are good. First impressions are also cheap, and there&rsquo;s a fair way to go from here. We need proper test systems. We&rsquo;ll probably have to adjust some of our own flows to fit theirs. There are decisions still to settle on both sides. That&rsquo;ll be a process rather than a sprint, and I&rsquo;d rather say so now than write a triumphant post about it in week two.</p>
<p><strong>Lightspeed</strong> is a player-facing app we want to support as a payment option. Peter had already built a first version before I arrived, and taking it over was my excuse to get my hands into real code here for the first time. Ten thousand lines, read end to end, then run and poked at until I understood what it was doing. That produced a list of bugs and a stack of follow-up stories, which is what reading anything carefully produces. The more useful outcome is that it&rsquo;s in my head now as well as his.</p>
<p>And <strong>Verifone</strong> terminals on the desk, which is the regular reminder that a lot of our software ends in a physical object somebody has to touch.</p>
<p>The approach <a href="/en/author/jn/">John</a> and I settled on is to skip the big upfront specification. John is the other developer, though that undersells him, since he&rsquo;s as much solution architect and project manager as engineer. Get a test instance, build against it, let reality write the documentation. Two weeks in I don&rsquo;t know these systems well enough to write a good spec, and pretending otherwise would just produce a document we&rsquo;d quietly stop reading by September.</p>
<p>One small thing worth passing on to anyone thinking about working somewhere like this. I spent my first few days being patient about access, on the assumption there was a process I was supposed to wait for. Then I said out loud that this was more or less what I&rsquo;d been hired for, and could I please just have it. Half an hour later I had AWS. The waiting was entirely my own invention.</p>
<h2 id="how-we-ship">How we ship<a class="gn-heading-anchor" href="#how-we-ship" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>The most interesting problem of the first two weeks wasn&rsquo;t inside a repository. It was working out how changes get out to customers.</p>
<p>I spent a Friday on proper detective work. CloudWatch logs on one screen, a downloaded database snapshot on the other, lining transaction states up against what the code believes should have happened. Somewhere in there I found forty commits that had never been pushed anywhere. This is the unglamorous half of bringing knowledge in-house. Credentials are the easy part. Rebuilding an understanding of what the system actually does takes a lot longer.</p>
<p>We don&rsquo;t have a gradual rollout mechanism today. What we do have is customers who are physical places, and a handful of them are a short drive from the office. So the first ring is geography. A few nearby clubs get the new version first, and if it goes wrong the recovery plan is twenty minutes in a car with a spare terminal in the boot. That took about five minutes of conversation to arrive at, because once you notice where the customers are it&rsquo;s a fairly obvious move. It&rsquo;s not what I&rsquo;d draw on a whiteboard. It&rsquo;s available this week, and it buys enough confidence to keep shipping while the proper thing gets built.</p>
<h2 id="learning-the-game-and-the-words-for-it">Learning the game and the words for it<a class="gn-heading-anchor" href="#learning-the-game-and-the-words-for-it" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>On day three I played the first round of golf of my life, with colleagues. Seven or eight shots a hole. Never a ten. We didn&rsquo;t lose a single ball all round, which I&rsquo;m told is the more impressive statistic for a beginner. Nobody wrote down a score.</p>
<p>I&rsquo;d recommend the general version of that to anyone joining a company whose domain they don&rsquo;t know. Not golf specifically. Going and doing the thing the software is about.</p>
<p>The week after, John and I sat down for what turned into the most useful two hours of week two: agreeing what we call things, before three systems each invent their own word for the same concept. Nine holes is a loop. Stroke index ranks the holes by difficulty. There&rsquo;s rating category. None of this is exotic if you play golf, and all of it is invisible to an engineer who has never stood on a tee box, which two weeks ago was me.</p>
<p>Getting the vocabulary right early costs almost nothing. Getting it wrong costs you three services, three names for one thing, and a translation layer nobody set out to build. I did run one check I&rsquo;d suggest to anyone doing this: I had Claude verify &ldquo;loop&rdquo; on its own, because it smelled to me like a word engineers invent and then talk themselves into believing is domain language. It came back as real golf. Using the model to check our own vocabulary, rather than to write code, is one of the better uses I&rsquo;ve found for it so far.</p>
<h2 id="twenty-nine-items-and-where-this-is-going">Twenty-nine items, and where this is going<a class="gn-heading-anchor" href="#twenty-nine-items-and-where-this-is-going" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>The improvement list lives in ClickUp now, mostly so I can stop carrying it around in my head. Twenty-nine items, all still in the backlog, grouped roughly into repository standards and conventions, security and secrets, CI/CD and testing and dependencies, infrastructure and data, architecture direction, and AI and agents.</p>
<p>The first thing we&rsquo;ve actually rolled out is the least exciting item on the whole list. <a href="https://mise.jdx.dev/">mise</a>, which pins and installs the right language versions per project, so you clone a repo and the correct Java turns up on its own. With that many technologies spread across that many repositories, the tax you pay all day long is switching between them, and that particular tax is avoidable.</p>
<p>The goal hasn&rsquo;t changed since the letter. Build a landscape where context is cheap to pick up. If a new engineer, or a fresh AI session, can understand a service quickly, work can start immediately, by whoever picks it up. If they can&rsquo;t, every task starts with archaeology. Everything on that list of twenty-nine sits downstream of that one sentence.</p>
<h2 id="the-scorecard">The scorecard<a class="gn-heading-anchor" href="#the-scorecard" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Which brings me back to the letter, and the promise to mark my own homework in public.</p>
<p><strong>Stay on the keys.</strong> Held. Two terminal fixes shipped, pull requests reviewed, and I&rsquo;ve taken ownership of one of the apps. I&rsquo;m still building, which was the condition I set before I accepted the job.</p>
<p><strong>Rebuild the understanding, not just the access.</strong> Mostly. The CloudWatch afternoon, the database snapshot, the Lightspeed handover: that&rsquo;s the right shape of work. What I can&rsquo;t claim yet is being able to release anything in the stack without asking somebody.</p>
<p><strong>Candor in the open.</strong> Held, and easier than I expected, mostly because the room here makes it easy.</p>
<p>Now the other half.</p>
<p><strong>Goal one slipped.</strong> &ldquo;Make the work visible&rdquo; was first on the list and this blog was supposed to be how. Every post before this one was written before I&rsquo;d signed the contract, which turns out to be the easiest possible time to write about a company. Then I actually started, and for two weeks there were things in front of me that mattered more than a blog post. I&rsquo;m not going to call that a crisis. I did write that if it went quiet I should treat it as a warning sign, so: noted.</p>
<p><strong>The cold start is nowhere near five minutes.</strong> I set myself five minutes from clone to running code and told future me to go and time it. I haven&rsquo;t timed it properly, which is its own small failure. I&rsquo;ve cloned enough of these repositories by now to give a fair estimate though. Somewhere between two and six hours depending which one you pick, call it four on average. Getting the iOS build running took most of a day of Xcode, pods and emulators. mise is the first real step at closing that, and a step isn&rsquo;t a number.</p>
<p><strong>The shared AI kit is item twenty-nine of twenty-nine, and that part is on purpose.</strong> In the letter I called it the thing that turns &ldquo;AI-first&rdquo; from a slogan into shared tooling. Two weeks in I can see why it can&rsquo;t come first. A shared kit built on top of sixty-eight inconsistent repositories would mostly just encode the inconsistency. Foundation first, kit second, and I&rsquo;d defend that order. What I won&rsquo;t pretend is that the order is free. Until we get there, I&rsquo;m getting a lot of leverage out of a setup I&rsquo;ve spent a year tuning that nobody else here has yet.</p>
<p><strong>And the days ate the space to think.</strong> I wrote that the creative, playful time isn&rsquo;t a reward you earn once the serious work is done, that it <em>is</em> the serious work. Two weeks in, it&rsquo;s the first thing the calendar takes. One thing helps here, and it&rsquo;s specific to this industry: golf is seasonal. Clubs are busy now, so we&rsquo;re busy now. The platform goes quiet over the winter, and that&rsquo;s when the toolset for the 2027 season gets built. Having a shape to the year is more useful than I expected it to be.</p>
<p>The thing I got most wrong is also the one I&rsquo;m happiest about. I turned up braced for friction. I expected to have to earn access, argue for scope, make a case for every change. Almost none of that was there, and what resistance I did meet in the first fortnight I&rsquo;d brought with me.</p>
<p>Two weeks isn&rsquo;t long enough to conclude much. Ask me again in the winter, when the clubs go quiet and we find out what we actually do with the time.</p>
]]></content:encoded>
      <category>Engineering</category>
      <category>Product</category>
      <category>Culture</category>
    </item>
    <item>
      <title>Making Claude Code shut up (and other customizations that earned their keep)</title>
      <link>https://build.golfnext.com/en/posts/tuning-claude-code/</link>
      <pubDate>Wed, 24 Jun 2026 10:00:00 +0000</pubDate><dc:creator>Johann Böhler</dc:creator>
      <guid>https://build.golfnext.com/en/posts/tuning-claude-code/</guid>
      <description>The first thing I did with Claude Code was make it talk less. The small, shareable customizations that survived my own filter — terse output, a statusline, the right hooks — plus the ones I deliberately refuse to automate, and why managing the context window shouldn&#39;t be a skill at all.</description>
      <content:encoded><![CDATA[<p>The first thing I did with <a href="https://code.claude.com/docs">Claude Code</a> was make it talk less.</p>
<p>Claude Code is Anthropic&rsquo;s terminal coding assistant — you talk to it, and it reads, writes, and runs code in your project. Out of the box it&rsquo;s polite. &ldquo;Certainly! I&rsquo;d be happy to help you with that. The issue you&rsquo;re seeing is likely caused by&hellip;&rdquo; I read fast, I pay per token, and I don&rsquo;t need the warm-up. I switched that off. I run a community plugin, <a href="https://github.com/JuliusBrussee/caveman">caveman</a>, that strips responses to the bone: no articles, no pleasantries, every bit of technical substance intact. &ldquo;Bug in auth middleware. Token expiry check uses <code>&lt;</code> not <code>&lt;=</code>. Fix:&rdquo; — and then the fix.</p>
<p>It takes adjusting. Some of those compressed sentences make you stop and read twice; a fragment can carry more meaning than it first lets on, and you spend the saved second decoding it. But the trade lands well in my favor. I get to the point faster, the context window fills slower, and if a line is genuinely unclear there&rsquo;s always the escape hatch: ask it to expand. It never minds.</p>
<p>That mode is the smallest possible example of the thing this whole post is about. Claude Code out of the box is generic. It&rsquo;s built to be reasonable for everyone, which means it&rsquo;s optimal for no one. The leverage isn&rsquo;t the model itself — you can&rsquo;t change how good it is, only which one you run. The leverage is in bending the tool around your own loop.</p>
<h2 id="the-trap">The trap<a class="gn-heading-anchor" href="#the-trap" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Customization isn&rsquo;t free, and I want to be honest about the bill. You can pour weeks into a setup nobody else can read, including future you.</p>
<p>The tools that took configurability furthest are the ones everyone reveres: vim and git. Deep, powerful, endlessly tunable. And, I&rsquo;d argue gently, not actually that accessible. The depth is both the point and the price. Vim is the only tool I know with a meme about how to quit it — that should tell us something.</p>
<p>I hear the objection most about git: &ldquo;but git is so <em>easy</em>.&rdquo; Is it, though? It&rsquo;s easy right up until your first real merge conflict, or the afternoon you rebase the wrong thing and quietly rewrite your own history with no clean way back. Yes, you can see where you&rsquo;re trying to get to. So can someone holding the box for a fifty-thousand-piece puzzle, the finished picture printed right there on the lid. Seeing the goal is not the same as the path being accessible. Those are different claims, and we keep mixing them up.</p>
<p>Here&rsquo;s the belief, stated plainly instead of as a complaint: a great tool should be as accessible as it can possibly be, and <em>then</em>, having earned that, offer infinite depth to anyone who wants to dig. The accessible part is the whole job, not a consolation prize for beginners. Depth is the bonus you add on top.</p>
<p>My bar for any Claude Code customization is one question: <em>would I hand this to a teammate without an hour of briefing?</em> If the answer is no, it usually doesn&rsquo;t survive. My stack stays simple and stupid on purpose. Here&rsquo;s what&rsquo;s left after that filter.</p>
<h2 id="what-earned-a-slot">What earned a slot<a class="gn-heading-anchor" href="#what-earned-a-slot" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p><strong>The terse output mode.</strong> Covered above. The single highest-leverage change, because it touches every response. Less to read, fewer tokens burned, longer-lived context.</p>
<p><strong>The statusline.</strong> This matters more than it sounds. I usually have several things going at once, and I lose track of where I am — which repo, which task, how much budget I&rsquo;ve got left. My statusline shows it all in one strip: the model I&rsquo;m on, how full the context window is, where I stand against my 5-hour usage limit (Claude Code caps how much you can run in any rolling five-hour window), and my current directory. No clicking, no guessing. I glance down and I know my situation. For anyone juggling more than one thing at a time, it&rsquo;s the cheapest sanity I&rsquo;ve bought.</p>
<p><strong>The settings layer.</strong> This is where it gets personal, and where the options never end. My config plays a sound when Claude finishes so I can look away and get pinged back. It fires OS notifications. You can have it log the purpose of each session, track tokens and cost, whatever you decide is worth knowing. There&rsquo;s no &ldquo;right&rdquo; setup — the point is that the hooks exist (little commands the tool runs automatically at set moments, like when it finishes) and you bend them to <em>your</em> purpose. Mine is &ldquo;tell me when you&rsquo;re done.&rdquo; Yours might be something else entirely.</p>
<p>None of these is clever. That&rsquo;s the point. Each one removes a small, daily friction, and none of them needs explaining.</p>
<h3 id="the-actual-wiring">The actual wiring<a class="gn-heading-anchor" href="#the-actual-wiring" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h3>
<p>The sound is a <code>Stop</code> hook in <code>settings.json</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="s2">&#34;hooks&#34;</span><span class="err">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;Stop&#34;</span><span class="p">:</span> <span class="p">[{</span> <span class="nt">&#34;hooks&#34;</span><span class="p">:</span> <span class="p">[{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;command&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;command&#34;</span><span class="p">:</span> <span class="s2">&#34;afplay ~/.claude/sounds/done.mp3&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;async&#34;</span><span class="p">:</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl">  <span class="p">}]}]</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p><code>async: true</code> so it never blocks the session, and <code>afplay</code> is just the built-in macOS player — swap in whatever you like.</p>
<p>The statusline is barely more than that. Claude hands your script a blob of JSON on every render; the script prints a line. You point at it once:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="s2">&#34;statusLine&#34;</span><span class="err">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;command&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;command&#34;</span><span class="p">:</span> <span class="s2">&#34;bash ~/.claude/statusline.sh&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>and the script reads whatever fields it cares about off stdin:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">input</span><span class="o">=</span><span class="k">$(</span>cat<span class="k">)</span>                                                  <span class="c1"># session state, as JSON</span>
</span></span><span class="line"><span class="cl"><span class="nv">model</span><span class="o">=</span><span class="k">$(</span>jq -r <span class="s1">&#39;.model.display_name&#39;</span>                          <span class="o">&lt;&lt;&lt;</span><span class="s2">&#34;</span><span class="nv">$input</span><span class="s2">&#34;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="nv">ctx</span><span class="o">=</span><span class="k">$(</span>  jq -r <span class="s1">&#39;.context_window.used_percentage // 0 | floor&#39;</span> <span class="o">&lt;&lt;&lt;</span><span class="s2">&#34;</span><span class="nv">$input</span><span class="s2">&#34;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="nv">fh</span><span class="o">=</span><span class="k">$(</span>   jq -r <span class="s1">&#39;.rate_limits.five_hour.used_percentage // 0 | floor&#39;</span> <span class="o">&lt;&lt;&lt;</span><span class="s2">&#34;</span><span class="nv">$input</span><span class="s2">&#34;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="nv">dir</span><span class="o">=</span><span class="k">$(</span>  jq -r <span class="s1">&#39;.workspace.current_dir&#39;</span>                       <span class="o">&lt;&lt;&lt;</span><span class="s2">&#34;</span><span class="nv">$input</span><span class="s2">&#34;</span><span class="k">)</span>
</span></span></code></pre></div><p>From there it&rsquo;s just drawing strings. Mine renders a little colored bar for each percentage — green, then yellow, then red as it fills — and right-aligns the path, so the strip ends up looking like this:</p>
<pre tabindex="0"><code>Opus 4.8  xhigh  · CTX ████░░░░░░ 38%  · 5h ██░░░░░░░░ 21%        ~/dev/golfnext
</code></pre><p>(<code>xhigh</code> there is just the reasoning-effort level I keep the model at.) That&rsquo;s the whole thing. No framework, no dependency to keep current. A shell script and a JSON blob.</p>
<h2 id="what-i-wont-automate">What I won&rsquo;t automate<a class="gn-heading-anchor" href="#what-i-wont-automate" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>More automation isn&rsquo;t always a win. Some of it just moves the cost somewhere you can&rsquo;t see, and those are the ones I leave switched off.</p>
<p>You can wire Claude to lint and format every file the moment it edits it. Sounds tidy. It&rsquo;s the opposite. If a hook reformats a file right after the model touched it, the model&rsquo;s picture of that file is now stale — so the next time it works there, it has to re-read the whole thing to catch up. You&rsquo;ve traded a clean diff for a slower, token-hungrier session. I&rsquo;d rather it just run the linter once, at the very end, when the thinking is done.</p>
<p>Same logic on automatic model fallback. You can have it silently switch to a smaller model when your first choice is busy. I don&rsquo;t want that. I pick a model deliberately, usually because the task needs the bigger reasoning model — the one that pauses to work a problem through before answering. If it&rsquo;s overloaded, I&rsquo;d rather come back in two hours and write &ldquo;pick this up again&rdquo; than have something quietly hand my hard problem to a model that can&rsquo;t carry it. Convenience isn&rsquo;t worth a worse answer I didn&rsquo;t notice I was getting.</p>
<h2 id="the-window-should-be-a-non-event">The window should be a non-event<a class="gn-heading-anchor" href="#the-window-should-be-a-non-event" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>People talk a lot about managing the context window — compacting it (having the tool summarize the conversation so far to free up room), watching it, nursing it along. I try to never be in that conversation.</p>
<p>The way I avoid it is structural: push the heavy lifting onto subagents — separate helper instances of Claude Code, each with its own private context — and keep the main thread clean. A subagent goes off, burns its own context reading and searching, and hands back the short answer. The main session stays light. Done right, I almost never reach for compaction, because I never let the window get full in the first place.</p>
<p>But here&rsquo;s the honest part: I think the fact that any of us has to know this is a smell. Nobody should <em>need</em> to understand context windows to start being productive with this tool. It&rsquo;s a current limitation wearing the costume of a skill. A few years from now I&rsquo;d bet it&rsquo;s abstracted away entirely, and we&rsquo;ll talk about it the way we talk about managing our own RAM — which is to say, we don&rsquo;t. I optimize around it today, and I try not to mistake working around a limitation for something worth teaching.</p>
<p>Can I prove it? Not with numbers. I don&rsquo;t have a clean before-and-after, and I distrust anyone who quotes a tidy percentage for this. What I have is a feel: the window fills slower, the budget stretches further, sessions stay light longer. It&rsquo;s subjective, and I&rsquo;d notice the moment it was gone.</p>
<h2 id="how-to-find-your-own">How to find your own<a class="gn-heading-anchor" href="#how-to-find-your-own" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>The hard part isn&rsquo;t configuring anything. It&rsquo;s knowing what&rsquo;s even possible.</p>
<p>Claude Code moves fast — it feels like ten new commands a week. So fast that the model itself can&rsquo;t keep up. I&rsquo;ll ask it about a real slash command — &ldquo;what does <code>/goal</code> do, can you explain?&rdquo; — and it has no idea the command exists. There&rsquo;s something funny about that: the most capable thing I work with often can&rsquo;t tell you about features that shipped after its training cutoff. It&rsquo;s behind on itself, in the most literal way. You can&rsquo;t just ask the model what it can do. You have to go look — read the changelog, skim a guide, point it at the open web where the fresh stuff actually lives.</p>
<p>The next thing I want to try isn&rsquo;t a setting at all. I keep half a dozen projects in flight, so I&rsquo;m going to build one small meta-repo: a single place that points at all of them, that anyone — not just me — can navigate. Same instinct as everything else here: if it only works for me, it doesn&rsquo;t really work.</p>
<p>That&rsquo;s the whole filter, in the end. Take the two or three things that kill a real, repeated friction. Leave the rest, however clever it looks. The goal was never the most elaborate setup. It was the one you&rsquo;d still be happy to hand to someone else.</p>
]]></content:encoded>
      <category>AI</category>
      <category>Engineering</category>
    </item>
    <item>
      <title>The Loop Debate Is a Bad Word for Autonomy</title>
      <link>https://build.golfnext.com/en/posts/loops-autonomy/</link>
      <pubDate>Wed, 24 Jun 2026 00:00:00 +0000</pubDate><dc:creator>Johann Böhler</dc:creator>
      <guid>https://build.golfnext.com/en/posts/loops-autonomy/</guid>
      <description>Everyone&#39;s debating &#34;Claude Code loops.&#34; After tracking a month of real tasks — every token, every hour it would have cost by hand — I&#39;m fairly sure most of the argument is about the wrong word. It&#39;s not loops, it&#39;s autonomy. And like microservices a decade ago, it arrives one verifiable task at a time, gated by a substrate nobody has built yet.</description>
      <content:encoded><![CDATA[<p>There&rsquo;s a genre of post going around right now. You&rsquo;ve probably seen it. &ldquo;I don&rsquo;t write prompts anymore — I design loops.&rdquo; &ldquo;Loop engineering is the new prompt engineering.&rdquo; I read these closely — I&rsquo;m trying to learn this as fast as anyone, and the people writing them are usually well ahead of me. But I keep coming away with the same itch: I can&rsquo;t quite map the word onto what I actually do all day.</p>
<p>I run Claude Code most days. For people who haven&rsquo;t met it: Claude Code is Anthropic&rsquo;s coding agent — you give it a task in the terminal and it reads files, writes code, runs tests, and iterates, instead of just handing you a snippet to paste. The capability is real and I lean on it hard. What trips me up is the vocabulary, so I tried to ground it.</p>
<p>I did the boring thing: I tracked everything. Every real task I ran through Claude Code got two numbers next to it — what it cost in tokens, and a rough estimate of what the same work would have cost by hand, in time. A month of rows. It&rsquo;s the least glamorous artifact imaginable, and it&rsquo;s the thing that finally made the &ldquo;loops&rdquo; debate click into place for me.</p>
<p>What clicked — for me, at least — is that a lot of the argument is about the wrong word. &ldquo;Loop&rdquo; isn&rsquo;t the thing. Autonomy is. And autonomy doesn&rsquo;t arrive as a switch you flip; it arrives one task at a time, which, if you were around for the last big architecture argument, should feel oddly familiar.</p>
<h2 id="loop-is-four-different-things">&ldquo;Loop&rdquo; is four different things<a class="gn-heading-anchor" href="#loop-is-four-different-things" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Part of why the debate feels muddy is that &ldquo;loop&rdquo; is doing at least four jobs at once, and people argue past each other because they&rsquo;re each holding a different one.</p>
<p>There&rsquo;s the <strong>agent loop</strong> — think, act, observe, repeat. This is just how an agent works. It reads the situation, takes an action, looks at the result, decides what&rsquo;s next. It has always existed. It&rsquo;s not a technique, it&rsquo;s the definition.</p>
<p>There&rsquo;s <strong>Ralph</strong> — the bash hack. It&rsquo;s the community&rsquo;s name for the crudest possible loop: wrap the agent in a literal <code>while</code> loop in your shell and let it re-run the same prompt until it declares itself done. (The name is just folklore; the technique is the point.) Crude, occasionally effective, the duct tape of autonomy.</p>
<p>There are the <strong>features</strong> — slash-commands like <code>/loop</code> and <code>/goal</code>, baked into tools like Claude Code. They wrap that same idea in something more controlled than a bash one-liner.</p>
<p>And there&rsquo;s <strong>&ldquo;loop engineering&rdquo;</strong> — the philosophy. The discourse. The posts.</p>
<p>Four different altitudes, one word. No wonder it&rsquo;s confusing.</p>
<p>But here&rsquo;s the thing they all share, the line every one of these repeats in its own dialect: <strong>goal, verification, stop condition.</strong> That trio is the actual substance. Everything else is which layer someone happens to be staring at.</p>
<p>Make it concrete. Take the most boring useful thing I automate: addressing review comments on a pull request.</p>
<ul>
<li><strong>Goal:</strong> resolve every review comment on this PR.</li>
<li><strong>Verification:</strong> CI is green, and every comment thread is actually addressed — not &ldquo;I think I fixed it,&rdquo; but the check passes and the thread is closed.</li>
<li><strong>Stop condition:</strong> no open comments left, or it hits an iteration cap and hands back to me.</li>
</ul>
<p>That&rsquo;s a loop. It&rsquo;s also just a task with a clear definition of done and a rule for when to quit. The trio is the whole game.</p>
<p>And notice what makes that example work: a machine, not me, can tell whether the goal is met — the check is green or it isn&rsquo;t. That&rsquo;s what I&rsquo;ll keep calling <em>verification</em> — an automatic, judgment-free signal that the work is actually done. Hold onto the word. How much of it a task has is the thing that decides everything later.</p>
<h2 id="its-not-loops-its-autonomy">It&rsquo;s not loops. It&rsquo;s autonomy.<a class="gn-heading-anchor" href="#its-not-loops-its-autonomy" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Here&rsquo;s the part it took me a while to see. Read those three criteria again — goal, verification, stop condition — and ask: does anything in them change based on whether <em>I</em> press enter between each step, or a harness (the script or tool wrapping the agent) does it for me? No. Nothing. The goal is the goal, the verification is the verification, the stop condition is the stop condition; they&rsquo;re identical whether a human advances each iteration or a script does. The only variable is <strong>who turns the crank between steps</strong> — and that isn&rsquo;t a question about loops at all. It&rsquo;s a question about autonomy: how much you trust the thing to keep going without you in the chair.</p>
<p>Which makes sense once you see how little there is to a loop — near enough, it&rsquo;s this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">while</span> <span class="ow">not</span> <span class="n">done</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">observe</span><span class="p">()</span>              <span class="c1"># look at where things stand</span>
</span></span><span class="line"><span class="cl">    <span class="n">act_toward</span><span class="p">(</span><span class="n">goal</span><span class="p">)</span>       <span class="c1"># take the next step</span>
</span></span><span class="line"><span class="cl">    <span class="n">done</span> <span class="o">=</span> <span class="n">verified</span><span class="p">()</span>      <span class="c1"># the stop condition: is the goal actually met?</span>
</span></span></code></pre></div><p>That&rsquo;s the whole thing. So &ldquo;should I be designing loops&rdquo; is a bit of a category error — the loop is trivial, a <code>while</code> and an <code>if</code>. The hard parts hide behind the word, and there are two of them. The first: how far are you willing to step back from a running process? That&rsquo;s trust. The second, the one I almost never see named: once you <em>do</em> step back, where does the thing actually run, and can you afford to run it there? That&rsquo;s infrastructure — and unlike trust, it isn&rsquo;t really up to you yet, because the cheap, standard place to run autonomous agents doesn&rsquo;t exist.</p>
<h2 id="what-the-work-actually-costs">What the work actually costs<a class="gn-heading-anchor" href="#what-the-work-actually-costs" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>That second question — can you actually afford to run it — is the one nobody seems to blog about, probably because almost nobody thinks to measure it. So I did. My day-to-day with Claude Code is attended — I run it on my own machine, with me in the chair. But the number I wanted was the other one: what the same work would cost <em>metered</em>, pay-per-token, the way you&rsquo;d have to pay to run it in the cloud without me babysitting it. That&rsquo;s the expensive lane, and it&rsquo;s the one that decides whether unattended is even on the table. So for a month I logged the metered cost of every real task, next to an honest estimate of how long it would have taken me by hand.</p>
<p>Two hundred and twenty tasks. <strong>About $1,360 in metered cost — and, by my own estimate, close to 540 hours of equivalent by-hand work.</strong> Five hundred and forty hours is the better part of three months of full-time engineering. It went out the door in one.</p>
<p>Put any reasonable number on an engineer-hour and that metered $1,360 stands against work worth <strong>twenty to forty times</strong> as much — around thirty at a normal loaded rate, counting salary plus overhead rather than take-home. I don&rsquo;t need the multiplier to be exact: halve my hour estimates if you think they&rsquo;re generous and it&rsquo;s still an order of magnitude.</p>
<p>The shape of the spend matters as much as the size. The median task cost <strong>$2.45</strong>. Two-thirds of everything came in under five dollars — bug fixes, small features, flaky-CI config, dependency bumps. The expensive runs are rare and they&rsquo;re the meaty ones: a full feature, built end to end, lands in the tens of dollars, and the biggest single rows are features in the <strong>$40–55</strong> range. Stack several rounds of review on one of those and a complete change lifecycle can clear $100. Those are the outliers, not the norm.</p>
<p>The part that decides where to start: I tagged the rows that are pure review-and-fix work — addressing PR comments, fixing a failing build, resolving conflicts. <strong>That whole category was under a tenth of the total spend</strong> — most tasks a few cents to a couple of dollars, half of them under a dollar. And it rarely needed much from me. The comments are bounded, the fix is usually local, and the verification is already wired: the build is green or it isn&rsquo;t. It&rsquo;s the most checkable, lowest-judgment part of the cycle, which makes it the obvious first thing to let run on its own.</p>
<p>The real money leak isn&rsquo;t the price of any one task. It&rsquo;s churn — runs that burned effort on something that didn&rsquo;t need it. A change that should have cost twenty dollars cost a hundred because I let it grind on a problem I should have just steered it through. That waste, not the per-task price, is the thing worth engineering against.</p>
<h2 id="the-scary-number-is-mostly-a-metering-artifact">The scary number is mostly a metering artifact<a class="gn-heading-anchor" href="#the-scary-number-is-mostly-a-metering-artifact" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>There&rsquo;s a version of this where someone sees that monthly total and recoils. &ldquo;You spent <em>how much</em> on an AI?&rdquo; And I get it — $1,360 a month is a genuinely alarming figure.</p>
<p>But it&rsquo;s mostly an artifact of how it&rsquo;s counted, not of what the work is worth — and it isn&rsquo;t even what I pay. The $1,360 is the metered figure I measured. My actual bill is the flat subscription I run this on: a mid-tier &ldquo;Max&rdquo; plan, used at a normal pace — not a hammer-everything-through-the-model, let-me-Google-that-with-Claude-too pace, just steady real work — about €110 a month, call it $120. Same keystrokes, same output, same month of work; the metered figure is roughly ten times the subscription one, purely because of how it&rsquo;s billed.</p>
<p>And here&rsquo;s the bit that quietly defuses the whole cost panic: you work <em>attended</em> anyway. You&rsquo;re sitting there. You&rsquo;re steering. The expensive, unbounded, &ldquo;oh god what is it doing&rdquo; scenario is the <strong>unattended</strong> one — the agent grinding away on its own while the meter spins. Which is exactly the thing you&rsquo;re least ready to trust in the first place.</p>
<p>So the two big worries — &ldquo;it&rsquo;s too expensive&rdquo; and &ldquo;I can&rsquo;t let it run on its own yet&rdquo; — turn out to be the same frontier seen from two sides.</p>
<h2 id="so-what-do-you-actually-build">So what do you actually build?<a class="gn-heading-anchor" href="#so-what-do-you-actually-build" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Not an autonomous loop — not yet, and not for most of it. What I build instead is a <em>human-triggered chain of skills</em>, where &ldquo;skill&rdquo; just means a named, reusable task you can hand the agent, like a saved command. One of mine babysits pull requests: after I open a PR I kick it off, and it watches for the things that need a response — a new round of review comments, a failing build — and works them until the PR is green or it hits something it can&rsquo;t handle and taps me on the shoulder. I start it; it doesn&rsquo;t start itself.</p>
<p>The interesting part is what <em>doesn&rsquo;t</em> run the model. The watching is a plain shell loop polling GitHub every few minutes for a handful of cheap signals — open unresolved comment threads, the CI status, whether the PR&rsquo;s been approved — and it only wakes the agent when that set actually changes:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="c1"># the watcher is dumb and free — no model in this loop</span>
</span></span><span class="line"><span class="cl"><span class="k">while</span> pr_is_open<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="nv">signals</span><span class="o">=</span><span class="s2">&#34;</span><span class="k">$(</span>unresolved_threads<span class="k">)</span><span class="s2">::</span><span class="k">$(</span>failing_ci<span class="k">)</span><span class="s2">&#34;</span>   <span class="c1"># cheap GitHub calls</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="o">[</span> -n <span class="s2">&#34;</span><span class="nv">$signals</span><span class="s2">&#34;</span> <span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="o">[</span> <span class="s2">&#34;</span><span class="nv">$signals</span><span class="s2">&#34;</span> !<span class="o">=</span> <span class="s2">&#34;</span><span class="nv">$last</span><span class="s2">&#34;</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        wake_agent <span class="s2">&#34;</span><span class="nv">$signals</span><span class="s2">&#34;</span>     <span class="c1"># the only line that spends tokens</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl">    <span class="nv">last</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$signals</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    sleep <span class="m">600</span>                     <span class="c1"># 10 min; an idle PR costs nothing</span>
</span></span><span class="line"><span class="cl"><span class="k">done</span>
</span></span></code></pre></div><p>The agent only spins up when that set changes, and even then it does the bounded thing — read the new threads, fix the worthwhile ones in the working tree, tell a &ldquo;my code is broken&rdquo; build apart from an &ldquo;infra fell over&rdquo; one — then goes back to sleep. The expensive part, the model, sits gated behind the cheap part, a script that knows how to read a status. That gating is most of the trick to keeping these things affordable.</p>
<p>Mine is the pragmatic version, though, not the gospel one. Point it at someone else&rsquo;s repo and it needs real guardrails. A PR comment is untrusted input, which turns an open watcher into a prompt-injection surface: someone can leave a comment steering the agent toward something that has nothing to do with the PR, or just spam comments to keep waking it and quietly run up the bill. Past the toy stage you&rsquo;d want a classifier deciding what&rsquo;s even worth acting on, and an escalation path — an OS notification, a Slack ping, a ticket — for the things it shouldn&rsquo;t touch on its own. But you can get a surprising distance on the raw signals alone; &ldquo;open, unresolved comments plus CI status&rdquo; is already enough to be genuinely useful.</p>
<p>And the shape generalizes. Another one I keep meaning to finish: a weekly job that scans the open Dependabot and Renovate pull requests, merges and tidies the boring ones where nothing real has to change, and escalates the rest — the bumps that need an actual refactor, or where an API might break under you. Same skeleton every time: a cheap trigger, a bounded job, and a clear line where it hands back to a human. Building those loops, and the small toolkits around them, is quietly becoming its own kind of software engineering — less &ldquo;prompt the model,&rdquo; more &ldquo;design the harness the model runs inside.&rdquo;</p>
<p>Two rules I&rsquo;d carve in stone before building any of it. First: <strong>release is not a job for the model — it&rsquo;s your CI/CD.</strong> Don&rsquo;t teach an agent to do the thing your pipeline already does for free, deterministically, with an audit trail; that&rsquo;s not autonomy; it&rsquo;s reinventing a solved problem. Second: <strong>monitoring is not a job for the model — it&rsquo;s Sentry and Grafana.</strong> Don&rsquo;t pay a brain to watch a dashboard; wire the alert back to a trigger so the agent gets handed the problem only when something has actually broken. And whatever you build, cap the fix loop — a hard iteration limit plus a human &ldquo;good enough, ship it&rdquo; gate — or that $20 fix quietly becomes a $100 one while you&rsquo;re at lunch.</p>
<p>None of this is timidity. Human-in-the-loop is simply how you <em>start</em>: you loosen the leash one iteration at a time, as your own skills sharpen and, more importantly, as your verification gets good enough that you&rsquo;d trust the result without standing over it.</p>
<h2 id="weve-had-this-exact-argument-before">We&rsquo;ve had this exact argument before<a class="gn-heading-anchor" href="#weve-had-this-exact-argument-before" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>If all of this feels familiar, it&rsquo;s because we ran the entire play a decade ago. It was called microservices, and I was there for it. Quick recap if you missed that era: around 2014, teams started chopping big single applications — &ldquo;monoliths&rdquo; — into lots of small, independently deployable services. The loud argument was how <em>small</em> they should be.</p>
<p>The froth is identical. Back then the unanswerable question was &ldquo;how small is a microservice — can it be 200 lines?&rdquo; Today it&rsquo;s &ldquo;Ralph or <code>/goal</code>, and how many agents?&rdquo; In both cases it&rsquo;s a community sizing-debating its way through a thing before anyone yet knows what actually matters.</p>
<p>The good practice predated the name, too. We were splitting systems by domain — clean seams, owned boundaries — long before anyone said &ldquo;microservices.&rdquo; And the agent loop plus a verification harness existed long before anyone branded it &ldquo;loop engineering.&rdquo; The name showed up and took credit for work people were already doing.</p>
<p>And then it settled. The honest retrospective on microservices isn&rsquo;t &ldquo;we revolutionized everything.&rdquo; It&rsquo;s &ldquo;we mostly kept building the way we already built, with cleaner seams.&rdquo; Most teams went from a monolith to two or five services, not one to two hundred overnight. The change was real and it was undramatic, because most of what we built was already small.</p>
<p>But here&rsquo;s the correction that actually matters, the part the sizing-debate missed at the time. For microservices, the real delta was never the pattern. It was the <strong>substrate.</strong> Containers, then the cloud, then Kubernetes made small, independently deployable services cheap and standard to run. The pattern was downstream of the infrastructure — and the infrastructure showed up to meet it.</p>
<p>For loops, we&rsquo;re only halfway there. The capability substrate has arrived, and it&rsquo;s the <strong>model.</strong> One row on my sheet is a feature I&rsquo;d have budgeted two days of hand-work for, delivered for about fifty dollars. That was flatly impossible two years ago, at any price, with any loop. The loop didn&rsquo;t change; the thing inside it got good enough to trust with real work. That&rsquo;s the containers moment — the raw capability landing.</p>
<p>What hasn&rsquo;t arrived is the rest of the stack. There&rsquo;s no cheap, standard, safe place to actually run autonomy unattended — no equivalent of the managed cloud and Kubernetes that turned &ldquo;I have a small service&rdquo; into &ldquo;and here&rsquo;s the obvious, affordable place to run it.&rdquo; Nothing yet says &ldquo;let this agent grind on its own overnight without it costing a fortune or going off the rails.&rdquo;</p>
<p>So: the model is the containers; the Kubernetes hasn&rsquo;t been built yet. And &ldquo;loops&rdquo; — the philosophy, the sizing wars — is the <em>nanoservices</em> froth all over again: the &ldquo;how absurdly small can we make it?&rdquo; overreach, playing out on a stack that&rsquo;s still missing its bottom half.</p>
<h2 id="autonomy-is-granular-not-a-switch">Autonomy is granular, not a switch<a class="gn-heading-anchor" href="#autonomy-is-granular-not-a-switch" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Which brings me back to where I started. You don&rsquo;t flip from &ldquo;I watch everything&rdquo; to &ldquo;it runs itself.&rdquo; Nobody went from one service to two hundred in a weekend, and nobody sane is going from attended-everything to autonomous-everything either.</p>
<p>The leash comes off one verifiable task-class at a time, gated by exactly one thing: how checkable the verification is. It&rsquo;s already happening. A PR-watcher that auto-fixes failed builds is the leash already off — on a narrow, bounded, machine-checkable class where the cost of being wrong is low and a green check tells you the truth. Meanwhile feature design and production changes stay leashed for a long time, maybe forever, because the verification is judgment and judgment doesn&rsquo;t fit in a CI check.</p>
<p>So when you read &ldquo;I design loops, not prompts,&rdquo; understand where it&rsquo;s coming from. It&rsquo;s not a lie. But notice who tends to say it: people at the big AI labs — the ones building these models — or with effectively unlimited token budgets, or both. For them the missing substrate isn&rsquo;t missing; the meter doesn&rsquo;t apply the way it applies to you. &ldquo;Just run loops&rdquo; is a perfectly honest description of <em>their</em> reality. It&rsquo;s frontier-reporting: true where it&rsquo;s reported from, with the best tooling and the most checkable problems, sold as a universal prescription it isn&rsquo;t yet for anyone working against a real budget.</p>
<p>So I&rsquo;m not tuning out the debate — I&rsquo;m tuning out the word. I build the attended chain, harden my own skills and verification, and let each task-class earn its slack one check at a time. That&rsquo;s the part I can act on today, while the rest gets argued out.</p>
<p>Which is the same move that turned out right with microservices: clock the froth, let the sizing wars run, and keep shipping the boring version that actually works.</p>
]]></content:encoded>
      <category>AI</category>
    </item>
    <item>
      <title>A clean machine</title>
      <link>https://build.golfnext.com/en/posts/clean-machine/</link>
      <pubDate>Tue, 23 Jun 2026 00:00:00 +0000</pubDate><dc:creator>Johann Böhler</dc:creator>
      <guid>https://build.golfnext.com/en/posts/clean-machine/</guid>
      <description>Two years after a colleague&#39;s dotfiles scrolled past in my GitHub feed, a new laptop finally forced the issue. How I rebuilt my Mac as a repo with chezmoi, ran my once-a-year tool review as a conversation with Claude, and automated the parts I used to click through.</description>
      <content:encoded><![CDATA[<p>In the last post I set myself a goal for the first ninety days: a five-minute cold start. Fresh laptop to running code in under five minutes — clone the project, run it locally, hit a live endpoint, pass the tests, all before your coffee goes cold. The shortest honest path from a new hire&rsquo;s first morning to their first real contribution. That&rsquo;s the on-ramp I want to build for the team at GolfNext, the Danish golf-tech company I&rsquo;m about to join.</p>
<p>Before I can hand that to anyone else, I owe it to my own machine. A new job means a new MacBook is on the way, so I pointed the same goal at myself first.</p>
<h2 id="the-two-year-old-bookmark">The two-year-old bookmark<a class="gn-heading-anchor" href="#the-two-year-old-bookmark" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>About two years ago, a <a href="https://github.com/brandstetterm/dotfiles">dotfiles repo</a> rolled past in my GitHub timeline — the feed of what the people you follow are working on. It belonged to Manuel Brandstetter, a former colleague and one of the best engineers I&rsquo;ve worked alongside. He took over as lead of <a href="https://scrumlr.io">scrumlr.io</a>, the open-source retrospective tool I helped build, after I left inovex, the German consultancy where we&rsquo;d worked together. So when his setup scrolled by, I stopped to look. No grand pitch, just there in passing: everything that makes a laptop <em>his</em> laptop, version-controlled in one place. I remember thinking, clearly and out loud, &ldquo;I should do that.&rdquo; Then I didn&rsquo;t. For two years.</p>
<p>If you&rsquo;ve never kept dotfiles: they&rsquo;re the small config files that quietly shape your machine. Your shell setup, your editor settings, your git config — the dozens of little preferences you&rsquo;d otherwise rebuild by hand, from memory, every time you get a new computer. A dotfiles repo puts them under version control so the machine becomes reproducible instead of hand-crafted and forgotten.</p>
<p>The reason I never got around to it is the boring one: it always felt like an afternoon I could spend on something more urgent. The thing about a brand-new laptop, though, is that it removes the excuse. You&rsquo;re going to set the machine up anyway. You may as well set it up <em>once</em>, properly, in a way you never have to repeat.</p>
<h2 id="the-audit-i-run-every-december">The audit I run every December<a class="gn-heading-anchor" href="#the-audit-i-run-every-december" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>There&rsquo;s a habit I&rsquo;ve kept for years. Somewhere in the quiet week around Christmas, I sit down and audit my tools. Am I still using the best thing for each job, or just the thing I happened to pick three years ago and never questioned? It&rsquo;s usually a solo afternoon — reading changelogs, half-remembered Hacker News threads, a bit of &ldquo;what&rsquo;s everyone else using now.&rdquo;</p>
<p>This year had a running start. A few weeks earlier I&rsquo;d gone looking for something like nvm, but for <em>every</em> runtime, not just Node: Python, Go, Java, the lot. I found <a href="https://mise.jdx.dev">mise</a>, which does exactly that, and swapping a drawer full of per-language version managers for a single one was the most satisfying thing I&rsquo;d done to my setup in months. That small win, I suspect, is what quietly tipped a two-year-old &ldquo;I should do that&rdquo; into &ldquo;do it now.&rdquo;</p>
<p>So this year I ran the audit as a conversation instead. I pointed Claude — Anthropic&rsquo;s AI assistant, the same one I&rsquo;ll be building with at work — at my current setup and asked the blunt version of the question: what here is dated, what&rsquo;s missing, what would you change, and why. Then I let it research and argue its case.</p>
<p>I kept the result honest by making it a diff. The old setup stayed on my <code>main</code> branch; the reviewed one went on a branch I called <code>cleanup</code>. So every recommendation was a line I could read, accept, or reject, not a black box rewriting my machine while I wasn&rsquo;t looking. That framing mattered more than I expected. A review you can read line by line is a review you actually trust.</p>
<p>Two things came out of it: a way to treat the whole machine as one repo, and a short list of tools I&rsquo;d been silly to skip.</p>
<h2 id="chezmoi-the-machine-as-a-repo">chezmoi: the machine as a repo<a class="gn-heading-anchor" href="#chezmoi-the-machine-as-a-repo" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>The manager is <a href="https://chezmoi.io">chezmoi</a>. It tracks your dotfiles in a git repo and renders them onto any machine you own — <em>chez moi</em>, fittingly, French for &ldquo;at my place.&rdquo;</p>
<p>What sold me over a plain folder of symlinks is the model. The files in the repo are the source of truth; you <code>apply</code> them to write the real files into your home directory. Edit, preview the diff, apply, commit. It templates (one file can adapt to a work machine versus a personal one), and it keeps secrets out of the repo entirely — my SSH keys stay inside <a href="https://developer.1password.com/docs/ssh/">1Password</a>, handed to git on demand, never written to a file chezmoi could accidentally commit.</p>
<p>The part that finally delivered on the two-year promise is the bootstrap. On a clean Mac, the whole machine comes back from three commands:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">brew install gh chezmoi
</span></span><span class="line"><span class="cl">gh auth login                              <span class="c1"># sign in to GitHub in the browser</span>
</span></span><span class="line"><span class="cl">chezmoi init --apply bitionaire/dotfiles   <span class="c1"># clone + render everything into place</span>
</span></span></code></pre></div><p>That last line clones the repo, installs my shell framework, lays down every config file, and runs a set of first-time setup scripts. One naming convention does most of the magic: a file called <code>dot_zshrc</code> in the repo becomes <code>~/.zshrc</code> on the machine. <code>dot_config/ghostty/config</code> becomes <code>~/.config/ghostty/config</code>. The <code>dot_</code> prefix is just chezmoi&rsquo;s way of writing a leading dot without confusing your file browser. Once that clicks, the repo reads like a mirror of your home directory.</p>
<h2 id="the-tools-the-review-actually-changed">The tools the review actually changed<a class="gn-heading-anchor" href="#the-tools-the-review-actually-changed" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>I already ran <a href="https://ohmyz.sh">oh-my-zsh</a> with the <a href="https://github.com/romkatv/powerlevel10k">powerlevel10k</a> prompt. The review pushed three additions hard, and was right about all three:</p>
<ul>
<li><strong><a href="https://ghostty.org">Ghostty</a></strong> — a fast, modern terminal. I&rsquo;d been on the macOS default for years out of pure inertia. Ghostty is quicker, themeable in a plain text file (which means chezmoi can track it), and renders the powerline glyphs my prompt uses without a separate Nerd Font install.</li>
<li><strong><a href="https://github.com/junegunn/fzf">fzf</a></strong> — a fuzzy finder, and the one that changed my day-to-day most. <code>Ctrl-R</code> turns shell history from a blind up-arrow hunt into a live fuzzy search. <code>Ctrl-T</code> drops a file path into whatever you&rsquo;re typing. Pipe anything into it — <code>git branch | fzf</code> — and you get an interactive picker for free.</li>
<li><strong><a href="https://github.com/ajeetdsouza/zoxide">zoxide</a></strong> — a smarter <code>cd</code>. It learns the directories you actually use and lets you jump by a fragment of the name. <code>z dot</code> lands me in my dotfiles repo from anywhere on the machine, no matter how deep it&rsquo;s buried.</li>
</ul>
<p>None of these are exotic — they&rsquo;re tools a lot of terminal-heavy people already swear by, and I&rsquo;d simply never sat down to add them. Wiring them in is a few lines of shell config:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="c1"># fzf — fuzzy finder (Ctrl-R history search, Ctrl-T file picker)</span>
</span></span><span class="line"><span class="cl"><span class="nb">command</span> -v fzf &gt;/dev/null 2&gt;<span class="p">&amp;</span><span class="m">1</span> <span class="o">&amp;&amp;</span> <span class="nb">eval</span> <span class="s2">&#34;</span><span class="k">$(</span>fzf --zsh<span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># zoxide — smarter cd (`z &lt;dir&gt;` jumps by frequency + recency)</span>
</span></span><span class="line"><span class="cl"><span class="nb">command</span> -v zoxide &gt;/dev/null 2&gt;<span class="p">&amp;</span><span class="m">1</span> <span class="o">&amp;&amp;</span> <span class="nb">eval</span> <span class="s2">&#34;</span><span class="k">$(</span>zoxide init zsh<span class="k">)</span><span class="s2">&#34;</span>
</span></span></code></pre></div><h2 id="automating-the-parts-i-used-to-click-through">Automating the parts I used to click through<a class="gn-heading-anchor" href="#automating-the-parts-i-used-to-click-through" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>A fresh Mac isn&rsquo;t just dotfiles. It&rsquo;s an afternoon of downloading apps, dragging them to Applications, and clicking through the same System Settings toggles you&rsquo;ve set on every Mac you&rsquo;ve ever owned. That&rsquo;s the part the review automated, and it&rsquo;s the part I&rsquo;m proudest of.</p>
<p>Apps come from a single <code>Brewfile</code> — a manifest <a href="https://brew.sh">Homebrew</a> reads to install everything in one shot:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">brew bundle --file<span class="o">=</span>Brewfile   <span class="c1"># installs every CLI tool, app, and App Store entry</span>
</span></span></code></pre></div><p>One file lists the command-line tools, the desktop apps (1Password, Firefox, Ghostty, Docker, the editor), and even Mac App Store purchases. New machine, one command, twenty-odd apps install themselves.</p>
<p>Then there are the macOS settings. chezmoi runs setup scripts on first apply, and one of them is just a list of <code>defaults write</code> commands — the scriptable side of System Settings. A taste:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">defaults write -g KeyRepeat -int <span class="m">2</span>                            <span class="c1"># fast key repeat</span>
</span></span><span class="line"><span class="cl">defaults write -g ApplePressAndHoldEnabled -bool <span class="nb">false</span>        <span class="c1"># hold a key to repeat it</span>
</span></span><span class="line"><span class="cl">defaults write com.apple.finder AppleShowAllFiles -bool <span class="nb">true</span>  <span class="c1"># show hidden files</span>
</span></span><span class="line"><span class="cl">defaults write com.apple.finder _FXSortFoldersFirst -bool <span class="nb">true</span>  <span class="c1"># folders on top</span>
</span></span></code></pre></div><p>I&rsquo;d set every one of these by hand on every Mac for a decade and never thought to write them down — now they&rsquo;re a file. It isn&rsquo;t total. Apple deliberately blocks scripting the security-sensitive bits, so granting accessibility permissions and signing into iCloud stays manual. But the dozens of little toggles that used to eat an afternoon now take seconds, and I&rsquo;ll never have to remember them again.</p>
<h2 id="a-good-review-cuts-as-much-as-it-adds">A good review cuts as much as it adds<a class="gn-heading-anchor" href="#a-good-review-cuts-as-much-as-it-adds" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>The surprise was how much the conversation talked me <em>out</em> of. A tool audit isn&rsquo;t only about new toys; it&rsquo;s about noticing the cruft you&rsquo;ve been carrying.</p>
<p>So the <code>cleanup</code> branch deleted things. The handful of older version managers I&rsquo;d accumulated, plus a hand-installed Python, all folded into the one mise I&rsquo;d adopted a few weeks before. A shortcut I&rsquo;d wired up to work around an old Docker quirk that&rsquo;s since been fixed — gone. A tangle of work-versus-personal git config I&rsquo;d accreted across jobs, flattened back to a single identity. Each of those was a small decision I&rsquo;d made years ago for a good reason, then never revisited once the reason expired.</p>
<p>That&rsquo;s the real value of doing this with a sparring partner. Left alone, I tend to add. Asked to justify each piece out loud, I finally removed.</p>
<h2 id="what-im-taking-from-it">What I&rsquo;m taking from it<a class="gn-heading-anchor" href="#what-im-taking-from-it" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>The impressive part wasn&rsquo;t that an AI can write a config file — everyone knows it can now. It was the review. It didn&rsquo;t hand me a generic &ldquo;best setup&rdquo;; it looked at <em>mine</em>, asked what I actually do all day, and argued for changes that fit, with the reasoning attached so I could push back where it was wrong. Less a tool than the December audit I already run, with someone knowledgeable sitting across the table.</p>
<p>And it closed a loop. The five-minute cold start I want to give the GolfNext team is this same thing, scaled up: a new person, a new laptop, and a short path to real work without a week of setup tax. I built the personal version first.</p>
<p>So if you&rsquo;ve got your own &ldquo;I should do that&rdquo; bookmark gathering dust — a repo that once scrolled past in your timeline, maybe — this is your sign. Wait for the next clean machine. Then set it up once. Two years late still beats never.</p>
<h2 id="references">References<a class="gn-heading-anchor" href="#references" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>The tools and docs worth bookmarking, roughly in the order they show up above:</p>
<ul>
<li><strong><a href="https://chezmoi.io">chezmoi</a></strong> — dotfiles manager. Start with the <a href="https://www.chezmoi.io/quick-start/">quick start</a> and the <a href="https://www.chezmoi.io/user-guide/setup/">user guide</a>.</li>
<li><strong><a href="https://brew.sh">Homebrew</a></strong> and the <strong><a href="https://github.com/Homebrew/homebrew-bundle">Brewfile / <code>brew bundle</code></a></strong> — install apps and CLI tools from one manifest.</li>
<li><strong><a href="https://ohmyz.sh">oh-my-zsh</a></strong> — Z shell framework, with the <strong><a href="https://github.com/romkatv/powerlevel10k">powerlevel10k</a></strong> prompt theme.</li>
<li><strong><a href="https://ghostty.org">Ghostty</a></strong> — fast, config-as-text terminal.</li>
<li><strong><a href="https://github.com/junegunn/fzf">fzf</a></strong> — command-line fuzzy finder.</li>
<li><strong><a href="https://github.com/ajeetdsouza/zoxide">zoxide</a></strong> — a smarter <code>cd</code> that learns your habits.</li>
<li><strong><a href="https://mise.jdx.dev">mise</a></strong> — one version manager for node, python, go, and the rest.</li>
<li><strong><a href="https://developer.1password.com/docs/ssh/">1Password SSH agent</a></strong> — keep private keys out of the repo and off disk.</li>
<li>The repo that started it: <strong><a href="https://github.com/brandstetterm/dotfiles">Manuel Brandstetter&rsquo;s dotfiles</a></strong>.</li>
</ul>
<h2 id="ps--a-dock-trick-that-has-nothing-to-do-with-dotfiles">P.S. — a Dock trick that has nothing to do with dotfiles<a class="gn-heading-anchor" href="#ps--a-dock-trick-that-has-nothing-to-do-with-dotfiles" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>One freebie for the Mac users who made it this far. It has nothing to do with any of the above, but it&rsquo;s the kind of small thing that&rsquo;s quietly pleased me for years: you can drop blank spacers into the Dock to break your apps into clusters. Same <code>defaults write</code> trick as the rest of the post. Run it once per spacer you want:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="c1"># full-height spacer (run again for each extra one)</span>
</span></span><span class="line"><span class="cl">defaults write com.apple.dock persistent-apps -array-add <span class="s1">&#39;{tile-data={}; tile-type=&#34;spacer-tile&#34;;}&#39;</span> <span class="o">&amp;&amp;</span> killall Dock
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># half-height spacer (run again for each extra one)</span>
</span></span><span class="line"><span class="cl">defaults write com.apple.dock persistent-apps -array-add <span class="s1">&#39;{&#34;tile-type&#34;=&#34;small-spacer-tile&#34;;}&#39;</span> <span class="o">&amp;&amp;</span> killall Dock
</span></span></code></pre></div><p>Drag the spacers around like any other Dock icon, or drag one off the Dock to remove it. Tiny thing. Makes a crowded Dock feel organized.</p>
]]></content:encoded>
      <category>Engineering</category>
      <category>Infrastructure</category>
    </item>
    <item>
      <title>A letter to myself, before day one</title>
      <link>https://build.golfnext.com/en/posts/letter-day-one/</link>
      <pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><dc:creator>Johann Böhler</dc:creator>
      <guid>https://build.golfnext.com/en/posts/letter-day-one/</guid>
      <description>Before my first day at GolfNext — and before I&#39;ve even signed — what I&#39;m walking into, why I&#39;m leaving a job I liked, the goals I&#39;m setting myself for the first ninety days, and the compass I&#39;m bringing with me.</description>
      <content:encoded><![CDATA[<p><em>&ldquo;I may have something of interest to you, Johann. Would be great to connect 👍🏼.&rdquo;</em></p>
<p>That&rsquo;s the whole message. It arrived on LinkedIn out of nowhere, from a recruiter named Joe — which, given how often these things come in under a borrowed first name, might even be his real one. No company, no role, no details. The kind of message you&rsquo;d normally archive without a second thought.</p>
<p>This one ends with me leaving a job I like, at a company I have nothing bad to say about, for a company whose name Joe wouldn&rsquo;t even tell me. I had to find that out myself. Well — myself, and an AI.</p>
<p>I&rsquo;m writing this before my first day of work. I haven&rsquo;t signed the contract yet; I don&rsquo;t even know my start date. Writing the company blog before I officially have the job sounds backwards. Maybe it is. I&rsquo;m doing it anyway, because I&rsquo;m carrying around more thoughts than I can hold, and because I want a record — something to read back in six months and hold up against whatever actually happened.</p>
<p>So this is two things at once. The story of how a two-line message turned into a job, and a letter to a future version of me.</p>
<p>A confession before we start: I&rsquo;ve never written a blog post in my life. I&rsquo;m starting now on purpose — writing pins down ideas that are otherwise just vapor. So here I am, thinking out loud.</p>
<h2 id="from-three-breadcrumbs-to-a-company">From three breadcrumbs to a company<a class="gn-heading-anchor" href="#from-three-breadcrumbs-to-a-company" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>I asked Joe what his message was about. It turned out to be a Lead Engineer role at a small but established company: the first cornerstone of an AI-first development team — one that builds software mostly by directing AI instead of typing every line by hand. Joe wouldn&rsquo;t name it. He gave me breadcrumbs instead — Danish sports tech, around since 2012, 400+ venues across 18 countries.</p>
<p>So I handed the breadcrumbs to Claude, Anthropic&rsquo;s AI assistant. In seconds it named the company: GolfNext, in Ry, a short drive from where I live in Denmark. They make the self-service kit golf clubs run on — more on that later.</p>
<p>That little exercise is more or less the job description. If you can point an AI at three vague clues and have it reconstruct the company behind them, that&rsquo;s the work. So I replied with a fully AI-generated message, openly labeled:</p>
<p><em>&ldquo;Full disclosure, Joe: this reply is AI-generated. Felt fitting for an AI-first pitch — and yes, I let the agent do the detective work too. 😄&rdquo;</em></p>
<p>They wrote back. A few days later I drove out to Ry and spent an hour at the GolfNext office, and by the time I left I knew I wanted it. The tell was the first thing they said when I sat down: <em>&ldquo;Let&rsquo;s just have a chat here and talk openly.&rdquo;</em> No theater.</p>
<p>Before committing, I wanted to know exactly what I&rsquo;d be walking into, so I asked. Thomas Herskind, who runs product at GolfNext, sent over an overview of the systems they run today. I read it and wrote back with everything I thought was worth doing first, point by point. It ran long. Very long.</p>
<p>Thomas&rsquo;s reply was six words. <em>&ldquo;Congrats on your first novel, Johann!&rdquo;</em></p>
<p>I laughed out loud. You learn a lot about a place from how it teases you before you&rsquo;ve even signed.</p>
<h2 id="the-70-ideas-before-this-one">The 70 ideas before this one<a class="gn-heading-anchor" href="#the-70-ideas-before-this-one" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>None of which explains why a two-line message with no details got my attention in the first place. For that, I have to rewind a few months — and first tell you who&rsquo;s writing, since this is the internet.</p>
<p>I&rsquo;m a generalist. A full-stack engineer who never minded picking up whatever a project happened to need. Sixteen years in: a lot of backend, a lot of web apps, a few years leading a team, and co-creator of scrumlr, an open-source tool teams use to run retrospectives — those meetings where you look back at what went well and what didn&rsquo;t. I&rsquo;ve never been the engineer who loves technology for its own sake. What pulls me is the other end of the wire: the user, the design, the value, the thing that makes somebody&rsquo;s day a little easier. And getting there without a lot of detours. That&rsquo;s the CV in a paragraph. The itch is the part it can&rsquo;t carry.</p>
<p>I&rsquo;d had an itch for a while to build something of my own. Anything that might grow into a business, really — and an excuse to push Claude as far as it would go and find out how much real software it could actually build. The first project came out of a pet peeve I&rsquo;d carried across jobs. Release notes — the &ldquo;what changed&rdquo; list that&rsquo;s supposed to be for the people using the software — keep getting dumped into commit messages, the terse notes developers leave each other. <em>&ldquo;Upgrade ESLint to version 10.&rdquo;</em> Who is that for? So I built a small product that lets you write proper change entries in plain language, right next to the code, and hand them to the product managers who should own them. Built from scratch in about two weeks — the parts users see, the backend behind them, the pipeline that ships updates on its own. All of it, with AI doing most of the typing.</p>
<p>That felt good enough that I got greedy and reached for something much bigger: an agent that keeps software projects alive. Maintaining them, extending them, mostly on its own, with the release-notes tool as its first patient. That&rsquo;s where I hit a wall, and the wall taught me more than most of my finished projects did.</p>
<p>Some of it was scope. I&rsquo;d wrapped a full product around something that should have started as a small experiment. But the deeper lesson was about the tool. Today&rsquo;s AI is genuinely brilliant at a blank page, and at small, well-bounded changes. It gets shaky the moment you ask it to tear into big chunks of core logic. It starts to hallucinate — confidently inventing things that aren&rsquo;t there. It never quite cleans up after itself. Old context lingers and creeps back into the next attempt. A build that ambitious needs a clear plan for <em>how</em> you&rsquo;ll get there before you write a single line, and holding that plan together on my own, in stolen evenings and weekends, just wasn&rsquo;t something I could pull off. That project is parked, not buried.</p>
<p>So I changed tactics. I put Claude to work as a sparring partner on smaller things, the kind I could finish in a couple of weeks. I&rsquo;d pitch an idea; it would go research the market and, more often than not, come back with the same answer — crowded, already funded, don&rsquo;t bother. Painful. Also useful.</p>
<p>After about 70 of those, I stopped pitching and asked the obvious question: so what <em>should</em> I do? The answer was refreshingly blunt. <em>Stop hunting for a clever product. Find someone with a real problem and help them build a software-driven process around it that saves them serious time.</em> And since it knows how I work, it sketched out roughly what that would look like. A few months later, Joe&rsquo;s two lines landed in my inbox — and the role behind them, once I asked, turned out to be a near-perfect match for that sketch. The message with no details was the thing I&rsquo;d been told to go find.</p>
<h2 id="what-its-actually-about">What it&rsquo;s actually about<a class="gn-heading-anchor" href="#what-its-actually-about" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>High time I told you what I&rsquo;d actually found. GolfNext builds self-service tools and hardware for golf clubs — picture a ball dispenser members work themselves at the range, or a system that takes the fiddly, error-prone business of selling and redeeming vouchers and makes it calm. For years, capable external partners built most of that software, and it carried the company a long way. The shift now, as they laid it out to me, is that GolfNext wants the engineering know-how to live inside the company: a team that understands the whole landscape from the inside, for the kind of steady, long-term growth that comes from owning what you depend on. No verdict on the people who built it so far — just the natural next step for a company that plans to be around for a long time.</p>
<p>I&rsquo;m one of the first pieces of that. The brief: absorb the landscape — the same one Thomas previewed in that overview — take over development bit by bit, and before long help build a team around it.</p>
<p>Here&rsquo;s a confession that sounds odd in an engineering blog: I&rsquo;ve barely written code by hand in months. Call it half a year on my own projects, a couple of months at work. Not because I stopped building. The building just moved up a level. More of my day now goes into directing AI, reviewing what it gives back, and designing the guardrails that keep its output safe and correct. It&rsquo;s where the craft is heading — and it&rsquo;s exactly the team GolfNext is asking me to build. Nobody can honestly tell you where all this lands; the ground moves every few weeks. But the direction is clear enough to point at, and pointing at it is the job.</p>
<p>That&rsquo;s the <em>what</em>. Before the <em>how</em>, there&rsquo;s a goodbye I owe.</p>
<h2 id="leaving-a-good-thing">Leaving a good thing<a class="gn-heading-anchor" href="#leaving-a-good-thing" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>First, the honest part nobody enjoys writing. Saying yes here means leaving Salling Group — one of Denmark&rsquo;s largest retailers — and I don&rsquo;t want a single word of this read as a complaint. Salling was the perfect first step when I landed in Denmark: a real culture, people who genuinely look out for each other, work I&rsquo;m proud of. I shipped things that mattered, under deadlines with no room for error, next to engineers I learned plenty from. I made good friends there. Part of me is sad to go.</p>
<p>The reason I&rsquo;m moving isn&rsquo;t a flaw in the place. It&rsquo;s a difference in what I&rsquo;m built for. At a company that size, every decision gets weighed by a lot of thoughtful people, which is exactly how a big retailer earns and keeps people&rsquo;s trust. But it means being close to the wheel is rare, and steering quickly rarer still. I want the wheel. I want to point at a direction and move. That itch is most of why I started building things on the side in the first place, and it&rsquo;s precisely what this new role hands me. Leaving a good thing for the right reason is still leaving a good thing. Both are true at once.</p>
<h2 id="the-goals-im-setting-for-myself">The goals I&rsquo;m setting for myself<a class="gn-heading-anchor" href="#the-goals-im-setting-for-myself" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Now for the <em>how</em>. The wall I hit on that side project is the whole reason the goals below look the way they do. If AI is strongest inside clear boundaries, the answer isn&rsquo;t to prompt harder. It&rsquo;s to build a landscape where the boundaries are obvious — where context is cheap to hand over, where a change is easy to scope small, where the guardrails catch what the model gets wrong before a human has to. Because that&rsquo;s where I think this is all heading: the scarce resource in software becomes how fast anyone — human or AI — can pick up a context they&rsquo;ve never seen. If a new engineer and a fresh AI session can both understand a service quickly, work can start immediately, by whoever picks it up. If they can&rsquo;t, every task starts with archaeology. Most of what follows is really about that one sentence.</p>
<p>One disclaimer that matters: I haven&rsquo;t started yet. These are the standards I&rsquo;ll hold myself to, not decisions I get to make alone — the order and the priorities get worked out with the people who&rsquo;ve been running these systems for years.</p>
<p><strong>Make the work visible.</strong> This blog is goal one. When you&rsquo;re building knowledge inside a team, the worst thing you can do is lock it away in private channels and individual heads. Writing it down in the open forces clarity, pulls in good people, keeps everyone honest. So it exists now. Future me: if these posts went quiet, take it as a warning sign.</p>
<p><strong>Rebuild the understanding, not just the access.</strong> Taking over from an external team isn&rsquo;t really about collecting credentials and passwords. It&rsquo;s about rebuilding the understanding from the inside. So: every account checked and put in my name, every doc read and sharpened until I can operate and release anything in the stack without phoning a friend. That&rsquo;s the unglamorous half of the ninety days, and it&rsquo;s the half everything else waits on.</p>
<p><strong>Build a landscape everyone can build in.</strong> This is the one I care about most. Concretely:</p>
<ul>
<li><strong>A five-minute cold start.</strong> Fresh laptop to running code: understand the project, run it locally, hit a live endpoint, pass the test suite, all in under five minutes. It&rsquo;s the on-ramp to building and testing, not a rulebook for how to write code. Future me, this one is measurable — go time it.</li>
<li><strong>Docs that live right next to the code</strong>, so context is one directory away instead of one Slack message away.</li>
<li><strong>Automation for anything we&rsquo;d otherwise do by hand twice.</strong></li>
<li><strong>Technical debt paid down where it&rsquo;s genuinely in the way</strong> — not a spring clean, just the things standing in the road.</li>
<li><strong>A shared kit of prompts and presets for the AI</strong>, so everyone starts from the same proven setup instead of rebuilding it each time. That&rsquo;s what turns &ldquo;AI-first&rdquo; into actual shared tooling instead of a slogan.</li>
</ul>
<p>The point isn&rsquo;t to be the only people who can build. It&rsquo;s to be the quality gate that turns a rough idea into something you can trust, and the helping hand that makes building safe and easy for everyone else.</p>
<p><strong>Stay on the keys.</strong> I said this to Joe before I&rsquo;d said it to anyone at GolfNext, because it would save us both time. I&rsquo;ve led a team before, twenty-four people, and I stepped back to building on purpose. <em>&ldquo;If &lsquo;Lead&rsquo; means setting technical direction while still building, I&rsquo;m very in. If it means drifting away from the code, I&rsquo;m the wrong guy.&rdquo;</em> That line started as a filter for the job. It&rsquo;s a goal now. Future me: if you&rsquo;ve drifted, you know what to do about it.</p>
<p><strong>Hire excellent people, and then trust them.</strong> Not in the first ninety days, and not my call alone, but it&rsquo;s where this is headed, and it shapes the early calls. A small team of excellent people needs almost no process. Keep the bar high even when you&rsquo;re short-handed. That part, at least, isn&rsquo;t me projecting my reading list onto them: when hiring came up in Ry, they told me they&rsquo;d rather run short-handed for a few months than bring in the wrong person just to add capacity. It told me more about the place than any pitch could have. Hand people context instead of approvals. Judge the work, not the noise. Default to yes, and stop to debate only at the doors you can&rsquo;t walk back through. And leave room — real room — to spitball, to argue, to build things that might not work. The creative, playful time isn&rsquo;t a reward you earn once the serious work is done. In this line of work, it <em>is</em> the serious work.</p>
<h2 id="a-compass-not-a-rulebook">A compass, not a rulebook<a class="gn-heading-anchor" href="#a-compass-not-a-rulebook" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>The goals are the plan. Underneath the plan sits a compass, and it&rsquo;s worth showing you, because it&rsquo;ll shape how I steer.</p>
<p>What I won&rsquo;t show up with is a manifesto. I don&rsquo;t believe a culture can be written into existence. It grows out of its environment and the people in it, day by day, in a thousand small moments nobody writes down — which is why it can only be led by example, never enforced. We&rsquo;ll work out GolfNext&rsquo;s actual values together, in the doing. And I mean <em>together</em> in both directions. I&rsquo;ll arrive with convictions — this post is full of them — but I expect to be changed as much as I change anything, and to pick up ways of working I haven&rsquo;t even considered yet.</p>
<p>Part of it I owe to inovex, the German consultancy where I spent seven years — the longest stretch of the sixteen. They had a triangle — Employees, Customers, Economy — and they meant every corner of it: fair pay, the freedom to choose the right tools for the job, sane and agile ways of working, genuine care for the people doing the work. Most of that I still hold to. My own emphasis just sits a little differently. Where their triangle puts employee happiness, mine puts <em>Craft</em>. Not because happiness doesn&rsquo;t matter, but because I&rsquo;ve come to think it grows out of doing great work together, rather than being something you can aim at head-on.</p>
<p>So the three points I steer by are <em>Customer</em> (the aim — solving the problem people would genuinely miss), <em>Craft</em> (the swing — building it well, and getting better by building it), and <em>Capital</em> (the long game — staying healthy enough to finish the round, and keeping the knowledge in the house so nothing important lives in a single head). Lean too hard on any one and you slice; get all three swinging together and the ball drops.</p>
<h2 id="what-ive-been-reading-and-what-i-took-from-it">What I&rsquo;ve been reading, and what I took from it<a class="gn-heading-anchor" href="#what-ive-been-reading-and-what-i-took-from-it" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>A fair number of the goals above — the high bar, the trust, the near-absence of process — I got from <em>No Rules Rules</em>, Reed Hastings and Erin Meyer&rsquo;s book on how Netflix is run. The argument, compressed: push talent density up until you have a team of excellent people, use candor to keep them honest with each other, and then start taking the rules away — because most rules exist to stop bad outcomes from average performers, and once you haven&rsquo;t got those, the rules cost more than they save.</p>
<p>The bit I keep coming back to is the expense policy. It comes down to a single instruction: <em>act in the best interest of the company.</em> That looks like a policy about receipts. It isn&rsquo;t. It&rsquo;s a statement about who&rsquo;s responsible. A rulebook hands you an answer somebody else worked out once, for a situation that might not be yours anymore. One instruction hands you the <em>goal</em> and makes you work out the answer for the situation you&rsquo;re actually standing in. That&rsquo;s more weight to carry. It&rsquo;s also what makes you a real part of the thing instead of an executor of other people&rsquo;s decisions — and it&rsquo;s where creativity and initiative come from. You don&rsquo;t get an unexpected good idea out of somebody following steps.</p>
<p><strong>The part that has to be practiced.</strong> None of that works without candor, and candor doesn&rsquo;t come free — for anyone. The pull toward smoothing things over is universal, and caring without saying the thing is the failure mode that feels kind and isn&rsquo;t. Take away the rules and keep the politeness and you don&rsquo;t get freedom. You get a team quietly drifting, with everyone guessing.</p>
<p>So I read <em>Radical Candor</em> as well, Kim Scott&rsquo;s book on exactly this — useful feedback needs both halves at once: you have to actually care about the person, and you have to actually say the thing. Hence this commitment, in writing, where anyone can hold me to it: expect me to be very clear about what I think, what I&rsquo;ve decided, and why — including when the answer is that I got it wrong.</p>
<p><strong>And yet — not a culture you can copy.</strong> One caveat over all of it. I don&rsquo;t want to import Netflix&rsquo;s culture wholesale. It grew out of a particular company, in a particular market, at a particular size, and plenty of what makes it work there doesn&rsquo;t travel. The book&rsquo;s famous sharp edge travels worst of all: the keeper test, where anyone a manager wouldn&rsquo;t fight to keep is let go with a generous severance. I&rsquo;d leave that part behind entirely. Talent density earned by hiring carefully is a different thing from talent density maintained by cutting — and an edge that hard has very American roots. It&rsquo;s not how I&rsquo;d want a small team in Denmark to feel. There&rsquo;s just as much worth taking from how good people at companies like Google or Apple have built and run their teams — and those cultures disagree with Netflix&rsquo;s in places, on purpose. Ours will be its own thing — worked out in Ry, not copied in from California. The real test of any of this is what everybody does when nobody&rsquo;s enforcing anything.</p>
<p>Which lands back where the expense policy started. Don&rsquo;t adopt somebody else&rsquo;s rulebook. Adopt the habit of asking what&rsquo;s genuinely best here, right now, with the people we&rsquo;ve actually got — and stay willing to change the answer when the situation does.</p>
<h2 id="a-note-to-future-me">A note to future me<a class="gn-heading-anchor" href="#a-note-to-future-me" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>By the time you read this, you&rsquo;ll know everything I don&rsquo;t. The start date. Whether those first ninety days went to plan. Which of these goals survived contact with reality, and which were sweetly naive. Some of it will look obvious in hindsight. Some of it will look funny.</p>
<p>I wanted it written down before the job could start shaping my answers — while it&rsquo;s all still conviction, before it hardens into experience.</p>
<p>Thomas already joked that my first long reply to him read like a novel. Consider this post the sequel. Longer, and this time public. Let&rsquo;s see how close I got.</p>
]]></content:encoded>
      <category>Culture</category>
      <category>AI</category>
    </item>
  </channel>
</rss>
