&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/da/</link>
    <description>Engineering, operations, and infrastructure at a small, ambitious team.</description>
    <language>da-DK</language>
    <copyright>© 2026 GolfNext ApS · Ry, Danmark</copyright><lastBuildDate>Wed, 14 Oct 2026 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://build.golfnext.com/da/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>AI unleashed</title>
      <link>https://build.golfnext.com/da/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/da/posts/ai-unleashed/</guid>
      <description>I august var AI for mig autocomplete og et chatvindue, og jeg var ikke imponeret. Nu giver jeg planer til op til tredive agenter ad gangen, på en sammenlægning af backends, der ikke må have nedetid. Agenterne kan kun løbe frit, fordi planen er stram: om hvordan jeg beslutter, hvad jeg overlader, hvorfor et par timers planlægning er den dyre del, og hvad reviewet fangede, da agenterne skrev Java i Go.</description>
      <content:encoded><![CDATA[<p>Jeg var ikke vant til at arbejde med AI, da jeg startede hos GolfNext i august. Jeg havde brugt GitHub Copilot før, både autocomplete, der foreslår de næste par linjer, mens man skriver, og chatten, hvor man kan prompte den, og jeg var ikke imponeret. Den kunne ikke løse mine opgaver for mig. Dengang byggede jeg en integration mod et system, hvis dokumentation var privat, bag et login, og der var ingen måde at få Copilot til at læse den. Så uanset om den færdiggjorde en linje eller svarede på en prompt, arbejdede den mod et API, den aldrig havde set, og jeg lavede selv det egentlige arbejde.</p>
<p>Min chef sagde, at jeg ville blive imponeret af Claude Code. Jeg tænkte, at det ville jeg tro på, når jeg så det. Så så jeg det.</p>
<p>Claude Code er ikke autocomplete, og det er heller ikke et chatvindue. Det er en agent: man giver den en opgave, og den læser koden, kører kommandoer, laver ændringer og tjekker sit eget arbejde. Den kan endda bruge min browser, når jeg lader den, så dokumentation bag et login er noget, den kan gå ind og læse, på samme måde som jeg ville. Den mur, jeg ramte med Copilot, er der simpelthen ikke længere. I de to måneder siden har min måde at arbejde på flyttet sig. I dag ville jeg kalde det agentic engineering. Jeg bruger min tid på at beslutte, hvad der skal bygges, og på at tjekke, at det blev bygget, og agenterne står for det meste af byggeriet ind imellem.</p>
<p>Sådan ser det ud på det største, jeg arbejder på.</p>
<p>Vi har to adminportaler. Hver har sin egen backend, sit eget login og sin egen database, og klubpersonale, der bruger begge, logger ind to gange. At lægge dem sammen til én står på roadmappet, og jeg har <a href="/da/posts/road-to-glory/">skrevet før</a> om den rækkefølge, vi valgte: først en bro mellem de to logins, så én frontend oven på begge backends, og til sidst selve backend-delene.</p>
<p>De to første trin er færdige. Frontenden fra den ene portal ligger nu inde i den anden, de to platforme taler sammen, og featureudviklingen fortsatte i begge hele vejen igennem. Ingen fik et servicevindue. Golfklubber kører på den her software hver dag, så der var ingen planlagt nedetid, og der kommer heller ingen i resten af forløbet.</p>
<p>Det næste trin er sværere. Den ene backend er skrevet i Java, og vi genopbygger den i Go. Når det er gjort, bliver den ene backend porteret ind i den anden, og vi ender med én samlet applikation. Igen uden at stoppe featureudviklingen, og igen uden et vindue.</p>
<p>Det meste af koden til det bliver ikke tastet af et menneske. Det er den del, folk spørger til, og det er den mindst interessante del. Den første beslutning om ethvert stykke arbejde er dets størrelse, for jeg bruger agenter på tre forskellige måder. Sammenlægningen af backends er det store tilfælde, og jeg når frem til det via de små og de mellemstore. Det, der ændrer sig mellem de tre, er, hvor meget af <em>tænkningen</em> jeg overlader, og hvornår jeg tager den tilbage.</p>
<h2 id="lille-kan-du-gøre-det-selv">Lille: kan du gøre det selv?<a class="gn-heading-anchor" href="#lille-kan-du-g%c3%b8re-det-selv" aria-label="Link til dette afsnit"><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>Ved en lille opgave stiller jeg agenten ét spørgsmål, før noget andet: kan du gøre det her selv?</p>
<p>Jeg siger ikke &ldquo;gør det&rdquo;. Jeg spørger, om den kan. Den læser koden, ser på, hvad opgaven berører, og fortæller mig, om den er sikker, eller om der er noget, den ikke ved. Det meste af tiden er svaret ja, og så gør den det. Nogle gange er svaret &ldquo;det her afhænger af, hvordan X opfører sig, og det kan jeg ikke se herfra&rdquo;, og det er præcis den sætning, jeg gerne vil høre, før den går i gang, og ikke bagefter.</p>
<p>Et typisk eksempel: en fejlrapport om en printer, der ikke printer. Softwaren vidste udmærket, at noget var gået galt, for den skrev fejlen i loggen. Den fortalte det bare aldrig til den person, der brugte den, som ikke fik nogen advarsel overhovedet og stod og ventede på en udskrift, der aldrig kom. Rettelsen er lille og klart afgrænset: find stedet, hvor fejlen bliver fanget, og vis den til brugeren i stedet for kun at logge den. Jeg stillede spørgsmålet, fik et ja, og agenten rettede den selv.</p>
<p>Spørgsmålet koster ingenting, og det ændrer måden, det kan gå galt på. En agent, der får besked på at gøre noget, gør noget. En agent, der bliver spurgt, om den kan, har lov til at sige nej.</p>
<h2 id="mellem-research-først-muligheder-bagefter">Mellem: research først, muligheder bagefter<a class="gn-heading-anchor" href="#mellem-research-f%c3%b8rst-muligheder-bagefter" aria-label="Link til dette afsnit"><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>Ved en mellemstor opgave går agenten først, men den bygger ikke noget. Den vurderer opgaven, laver researchen (den involverede kode, API-dokumentationen, hvordan det samme problem allerede er løst andre steder i vores repositories) og kommer tilbage med, hvad den fandt, og hvad den vil foreslå.</p>
<p>Et typisk eksempel: et featureønske fra en klub, der bruger en bestemt integration. Agenten kan læse integrationen, se hvad den allerede understøtter, og finde ud af, hvor ønsket ville passe ind. Men den kommer tilbage med spørgsmål i stedet for et svar. Kan det overhovedet lade sig gøre med det, integrationen stiller til rådighed? Hvis ja, hvilken del af integrationen skal så udvides? Eller er den ene klubs behov så anderledes, at det fortjener sit eget specialmodul, frem for at bøje en fælles integration omkring det?</p>
<p>Det er ikke spørgsmål, koden kan svare på. De afhænger af, hvor mange andre klubber der bruger integrationen, hvad vi er villige til at vedligeholde, og hvor produktet er på vej hen, og intet af det står skrevet nogen steder, agenten kan læse. Så den kan ikke løse opgaven uden mig, og den skal ikke lade som om, den kan. Det, den kan, er at gøre et vagt ønske til en kort liste af reelle valg med fordele og ulemper allerede regnet igennem. Så begynder jeg at forme løsningen: jeg vælger én, kombinerer to, eller smider dem alle ud, fordi researchen ændrede spørgsmålet.</p>
<h2 id="stort-et-par-timer-før-der-skrives-kode">Stort: et par timer, før der skrives kode<a class="gn-heading-anchor" href="#stort-et-par-timer-f%c3%b8r-der-skrives-kode" aria-label="Link til dette afsnit"><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>Ved et stort projekt, og at lægge to backends sammen, mens begge bliver ved med at levere features, er et stort projekt, bruger jeg et par timer på at planlægge med agenten, før noget bliver bygget.</p>
<p>En del af det er brainstorm. Det meste er mig, der leverer den kontekst, agenten ikke selv kan få fat i:</p>
<ul>
<li><strong>Arkitektur.</strong> Hvad taler med hvad, hvilke grænser skal holde, hvad er målbilledet, og hvilke kompromiser er tilladt på vejen derhen.</li>
<li><strong>Teknologi.</strong> Hvad vi standardiserer på, hvad vi bevæger os væk fra, og hvad der ikke er til diskussion.</li>
<li><strong>Formål.</strong> Hvorfor vi overhovedet gør det. Her: klubpersonale skal logge ind én gang, og vi skal vedligeholde ét system i stedet for to.</li>
<li><strong>Brugeroplevelse.</strong> Hvad personen i den anden ende ser under hvert trin af migreringen, også de trin, der kun er halvt færdige.</li>
</ul>
<p>Planen begynder sit liv i agentens kontekst, den igangværende samtale, vi lige har brugt to timer på at fylde. Det er et skrøbeligt sted at opbevare noget. Luk sessionen, og den er væk, og ingen andre på teamet kan se den, mens den varer.</p>
<p>Så det sidste, planlægningssessionen gør, er at skrive den ud, som opgaver i vores opgavesystem frem for et langt dokument. Hver opgave får sine underopgaver, og agenten registrerer afhængighederne mellem dem: hvad der skal findes, før en opgave kan begynde, og hvad der venter på, at den bliver færdig. Når jeg lukker sessionen, er planen holdt op med at være en samtale og blevet til noget, resten af teamet kan se, tage fat i og følge.</p>
<p>Før noget går i gang, hører teamet om det. Vi taler det kort igennem, og de hører løsningsforslaget. De snakke er korte, for vi er allerede enige om det meste af retningen. Nogle gange beder jeg også en kollega om at kigge på selve planen for at få en second opinion, for en plan, som kun én person og én agent har set, har haft præcis ét menneskeligt tjek.</p>
<p>Det her er den dyre del, og det skal det være. Hvert spørgsmål, der bliver besvaret her, er et spørgsmål, en agent ikke skal gætte på senere. Hver afhængighed, der bliver skrevet ned, er en kollision, der ikke sker, når tredive ting kører på én gang.</p>
<h2 id="udførelsen-er-bevidst-den-kedelige-del">Udførelsen er bevidst den kedelige del<a class="gn-heading-anchor" href="#udf%c3%b8relsen-er-bevidst-den-kedelige-del" aria-label="Link til dette afsnit"><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>Når planen findes, er udførelsen næsten et antiklimaks. Jeg giver den tilbage til agenten, og den starter subagenter (separate agenter, den selv starter og koordinerer, hver med sin egen opgave) på hver underopgave, hvis afhængigheder er opfyldt. Som regel et sted mellem ti og tredive ad gangen. Når én bliver færdig, kan det, der ventede på den, gå i gang.</p>
<p>Tredive agenter i én kodebase lyder som en opskrift på, at de falder over hinanden. Det, der holder dem adskilt, er planen. De afhængigheder, der blev skrevet ned under planlægningen, afgør, hvad der kan køre samtidig, og hvad der må vente, så to agenter ikke ender med at bygge oven på noget, der ikke findes endnu.</p>
<p>Det virker, fordi der ikke er noget vigtigt tilbage, som agenterne skal beslutte. En agent, der kører en veldefineret underopgave med sin kontekst skrevet ned ved siden af, laver præcis den slags arbejde, agenter er gode til. En agent, der kører en vag opgave, gætter med stor selvsikkerhed, og man finder først ud af bagefter, hvilke dele der var gæt.</p>
<p>Det er også derfor, jeg ikke stoler på ordet &ldquo;unleashed&rdquo;, når folk bruger det om AI. Agenterne kan kun løbe frit, fordi planen er stram.</p>
<h2 id="byggede-de-det-jeg-planlagde">Byggede de det, jeg planlagde?<a class="gn-heading-anchor" href="#byggede-de-det-jeg-planlagde" aria-label="Link til dette afsnit"><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>Når alle agenterne er færdige, tjekker jeg først, om de byggede det, jeg planlagde. Landede hver opgave faktisk, og hænger helheden sammen til det, vi designede, frem for en samling underopgaver, der hver især består deres egne tests?</p>
<p>Derefter reviewer jeg fra tre retninger:</p>
<ul>
<li><strong>Arkitektur.</strong> Holdt grænserne? Krydsede noget en grænse, det ikke burde, eller tilføjede stille og roligt en afhængighed, planen ikke havde?</li>
<li><strong>Kodekvalitet.</strong> Er det kode, nogen på teamet kan eje om et år? I tråd med resten af repositoriet, testet hvor det betyder noget, ingen smarte detaljer, som ingen bad om.</li>
<li><strong>Brugeroplevelse.</strong> Føles det rigtigt for den, der bruger det, også midt i migreringen, hvor halvdelen af systemet er gammelt og halvdelen nyt?</li>
</ul>
<p>To af dem, arkitektur og brugeroplevelse, er kontekst, jeg gav agenten fra starten, så reviewet tjekker i virkeligheden arbejdet mod det, jeg leverede. Hvis det bliver ved med at finde den samme slags problem, hører rettelsen som regel hjemme i den næste plan.</p>
<p>Den tydeligste fangst indtil videre kom fra selve backend-omskrivningen, en portering fra Java til Go. På papiret leverede agenterne det, der var planlagt: endpoints var der og gjorde det samme som de gamle. Men den første version var Java skrevet i Go. Hvor Java-koden lænede sig op ad et bibliotek, genopbyggede agenterne det bibliotek i Go i hånden, også når Gos eget standardbibliotek allerede klarede opgaven. Datoer var det oplagte eksempel. I stedet for at bruge Gos <code>time</code>-pakke, som håndterer dem helt fint, fik vi en hjemmelavet kopi af den måde, Java håndterer dato og tid på.</p>
<p>Intet af det får en test til at fejle. Det kompilerer, det kører, det giver de rigtige svar. Det er også kode, som ingen på teamet burde skulle vedligeholde: mere af den end nødvendigt, formet som et sprog, den ikke er skrevet i, og fremmed for enhver, der kender Go. Det er et kodekvalitetsproblem og et arkitekturproblem på én gang, og man fanger det kun ved at spørge &ldquo;er det sådan, vi ville bygge det?&rdquo; og ikke kun &ldquo;virker det?&rdquo;.</p>
<p>Rettelsen gik gennem det samme loop som alt andet. Jeg spurgte Claude, hvordan vi kunne slippe af med det hjemmelavede bibliotek og bruge Gos standardbibliotek i stedet, uden at miste noget. Den sidste del var den egentlige begrænsning. Klienter kalder allerede de endpoints, og hvert tidsstempel, de får tilbage, skal komme ud i præcis samme format efter porteringen som før. En portering, der er pænere indeni og ødelægger nogens datoparsing udenpå, er ikke færdig. Det blev rettet: det hjemmelavede bibliotek forsvandt, og tidsstemplerne forblev de samme.</p>
<h2 id="hvor-min-tid-blev-af">Hvor min tid blev af<a class="gn-heading-anchor" href="#hvor-min-tid-blev-af" aria-label="Link til dette afsnit"><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>Læg det sammen, og på et stort projekt er tastearbejdet næsten forsvundet fra min dag. Tilbage er de to ender: timers planlægning i starten og et grundigt review til sidst. Det er et andet arbejde, og der er ikke mindre af det. Men jeg får langt mere fra hånden.</p>
<p>Jeg tror, det er den rigtige måde at se AI-kodning på: som en compiler, ikke som noget, der er ude efter vores job. Ingen skriver maskinkode i hånden længere. Compileren overtog det for årtier siden, og den gjorde ikke programmører overflødige. Den flyttede arbejdet et niveau op, til at beslutte, hvad programmet skal gøre. Agenter er det næste trin op, med én stor forskel: en compiler gætter ikke. Derfor er reviewet til sidst ikke valgfrit.</p>
<p>Indtil videre, her hos os, er produktiviteten ikke blevet til færre mennesker. Den er blevet til større ambitioner. Ethvert team har en liste over ting, der klart er værd at lave, men som aldrig vinder over den næste kundeforespørgsel: bedre interne værktøjer, features kunderne ville sætte pris på, men aldrig bad højlydt om, nemmere onboarding af den næste, der starter. Nu har vi tid til dem.</p>
<p>I august var AI for mig autocomplete og et chatvindue, og jeg var ikke imponeret. To måneder senere planlægger jeg sammenlægningen af to backends, der ikke må tages ned, og giver planen til 10, 20 eller 30 agenter ad gangen.</p>
<p>Min chef havde ret. Jeg blev imponeret. Men det var ikke hastigheden på tastearbejdet, der imponerede mig. Det var at opdage, at når tastearbejdet holder op med at være jobbet, så er det, der er tilbage, planen og reviewet, den del, jeg altid hellere ville have lavet.</p>
]]></content:encoded>
      <category>AI</category>
      <category>Teknik</category>
      <category>Arkitektur</category>
    </item>
    <item>
      <title>Feeling like a hot chick</title>
      <link>https://build.golfnext.com/da/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/da/posts/feeling-like-a-hot-chick/</guid>
      <description>I slutningen af september skrev en klub under med os, næsten uden at nogen skulle sælge noget. Medlemmerne havde brugt vores kiosker i andre klubber og kom hjem og spurgte: &#34;Hvorfor ikke her?&#34; Det her indlæg handler om, hvad det gør ved én som udvikler: at arbejde på det produkt, alle gerne vil have, og hvorfor det får mig til at ville gøre mit bedste.</description>
      <content:encoded><![CDATA[<p>I slutningen af september lukkede en af vores salgschefer en aftale, der næsten ikke tog nogen tid. Peter Selmer, der grundlagde GolfNext i 2012, <a href="https://www.linkedin.com/posts/peterselmer_golf-golfindustry-golftech-share-7510606974385000451-XUrx/">skrev om det på LinkedIn</a>. Salgschefen behøvede ikke at overbevise nogen. Det havde klubbens medlemmer allerede gjort. De havde brugt vores kiosker i andre golfklubber, og så kom de hjem og spurgte deres egen klub: &ldquo;Hvorfor ikke her?&rdquo;</p>
<p>Jeg kommenterede under opslaget, at golfspillere, der beder deres egen klub om GolfNext, er det bedste signal, der findes. Jeg har tænkt på det lige siden, og det her indlæg er den lange version af den kommentar.</p>
<h2 id="den-alle-gerne-vil-have">Den, alle gerne vil have<a class="gn-heading-anchor" href="#den-alle-gerne-vil-have" aria-label="Link til dette afsnit"><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>Så, titlen. Den hot chick, det handler om her, er ikke mig. Det er produktet.</p>
<p>GolfNext laver software, som golfklubber kører på, og den del, golfspillerne ser mest, er selvbetjeningskiosken: en digital reception, der er åben døgnet rundt, så en spiller kan betale greenfee, leje udstyr eller købe i proshoppen, uden at der står nogen bag en skranke. Så vidt vi ved, findes der ikke noget lignende i golf.</p>
<p>Det meste software bliver kun bemærket, når det går i stykker. Ingen går ind i en bygning og spørger, hvem der har lavet dørhåndtagene. Men golfspillere bruger vores kiosk i én klub, og så kommer de til den næste klub og savner den. Og så spørger de efter den. Sådan ser det ud, når et produkt er attraktivt: folk vil have det, før nogen har solgt det til dem.</p>
<p>Peter sagde det i én sætning: golfspillere forventer nu, at check-in er hurtigt og enkelt, og de lægger mærke til det, når det ikke er.</p>
<h2 id="hvad-det-gør-ved-mig">Hvad det gør ved mig<a class="gn-heading-anchor" href="#hvad-det-g%c3%b8r-ved-mig" aria-label="Link til dette afsnit"><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>Kiosken er noget, vi selv laver. Kolleger her i huset bygger en del af hardwaren, og mine andre kolleger og jeg bygger softwaren, der kører på den, og alt omkring den. Så når golfspillere spørger efter den ved navn, spørger de efter noget, vi har lavet, og jeg ville lyve, hvis jeg sagde, at lidt af den tiltrækning ikke smitter af.</p>
<p>Det er en god følelse, og den gør noget ved den måde, jeg arbejder på. Når man ved, at folk spørger efter det, man laver, ved navn, vil man gerne leve op til det. En fejl, der andre steder ville blive et punkt i backloggen, holder op med at være det. En skærm, der er fin nok, bliver en skærm, jeg gerne vil have til at være god. Man begynder at tænke ud fra spilleren, der står ved kiosken klokken halv seks om morgenen, for det er den spiller, der tager hjem og fortæller sin egen klub om det. Eller lader være.</p>
<p>Ingen behøver at bede mig om at gøre mit bedste på et produkt som det. Jeg vil gerne.</p>
<h2 id="at-blive-ved-med-at-være-attraktiv-er-dagligt-arbejde">At blive ved med at være attraktiv er dagligt arbejde<a class="gn-heading-anchor" href="#at-blive-ved-med-at-v%c3%a6re-attraktiv-er-dagligt-arbejde" aria-label="Link til dette afsnit"><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>Produkter bliver ikke ved med at være attraktive af sig selv. Vi arbejder på det hele tiden, og det meste handler om fire ting.</p>
<p><strong>Nemt at bruge.</strong> For spilleren ved kiosken og for personalet bag skranken. Da en PDF med boldkoder, som klubberne printer, begyndte at fylde to sider, lå den ikke en måned i backloggen. Den blev rettet inden for en time. Små ting som det er forskellen på et produkt, folk finder sig i, og et, de spørger efter.</p>
<p><strong>Nemt at integrere.</strong> Klubberne kører allerede andre systemer, og de skal ikke vælge mellem dem og os. Jo nemmere vi er at sætte ind ved siden af det, en klub allerede har, jo færre grunde har den til at sige &ldquo;ikke endnu&rdquo;.</p>
<p><strong>Mindre kedeligt manuelt arbejde.</strong> Alle de fysiske enheder, vi har derude, kioskskærme, kortterminaler, boldautomater, bliver nu tilmeldt med en 12-cifret kode, der tastes ind på stedet, og får derefter deres indstillinger og opdateringer fra os. Ingen forbinder sig til en enhed og sætter den op i hånden længere. Næste skridt er en lille webside, hvor klubbens personale selv sætter en ny enhed op, så det kræver en pakke og et link at udskifte en defekt enhed i stedet for flere telefonopkald. Ingen har valgt det her job for at taste de samme indstillinger ind på den fyrretyvende maskine, og det har ingen i klubberne heller.</p>
<p><strong>Åbent, når alle er gået hjem.</strong> Det er den, der betyder mest for mig. Kiosken er en reception, der aldrig lukker. Når den sidste medarbejder har låst og er kørt hjem, kan en spiller stadig dukke op, betale greenfee og spille. Klubben tjener penge uden for åbningstiden, og golfspilleren får den samme bekvemmelighed, som vedkommende har alle andre steder i livet. Det virker kun, hvis produktet er pålideligt, når der ikke er nogen på stedet til at genstarte det.</p>
<p>Og så er der det at lytte. Når en klub fortæller os, at noget er en fejl, løser vi det som regel samme dag. Men nogle gange er det, én klub kalder en fejl, præcis det, en anden klub godt kan lide. Så vælger vi ikke en vinder. Vi gør det til en indstilling, og hver klub bestemmer selv. Alle er glade, og klubbens administratorer bevarer styringen over deres eget setup i stedet for at skulle bede os om at ændre det for dem. Et produkt, der er selvbetjening for golfspillerne, skal også være selvbetjening for dem, der driver klubben.</p>
<h2 id="hvor-det-bærer-hen">Hvor det bærer hen<a class="gn-heading-anchor" href="#hvor-det-b%c3%a6rer-hen" aria-label="Link til dette afsnit"><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>Intet af det her handler om at være et rart produkt for en håndfuld klubber. I august skrev jeg <a href="/da/posts/road-to-glory/">Road to glory</a>, vores plan for platformen under det hele, og der sagde jeg ambitionen højt: vi vil have, at det her bliver den platform, golfbranchen kører på. Hver klub, hver driving range, hvert land, der har begge dele.</p>
<p>Det indlæg handlede om motoren: færre sprog, én form for alle repositories, en kodebase, som alle på teamet kan åbne og være nyttige i. Jeg skrev også, at motoren ikke kan skabe det, der gør ambitionen realistisk. Klubber, der ser produktet, er imponerede, og den reaktion er aktivet. Peters fredagsaftale er den reaktion, udspillet i et salgsmøde.</p>
<p>Så motoren betyder noget på grund af det, den lader os gøre for de mennesker, der står foran skærmen. Og der kommer brugeroplevelsen først. Hver beslutning starter med den, der bruger det: golfspilleren ved kiosken før første starttid, medarbejderen, der skal forklare noget til et medlem, administratoren, der ændrer en indstilling sidst på en lang dag. Hvis en funktion er smart, men får nogen af dem til at stoppe op og tænke, er den ikke færdig. Den bedste kompliment, vores produkt kan få, er, at ingen lægger mærke til det, lige indtil de står i en klub, der ikke har det.</p>
<p>Det er den slags ambition, der driver mig. Ikke bare at vokse, men at vokse ved at være det, folk foretrækker.</p>
<h2 id="attraktiv-er-et-løfte">Attraktiv er et løfte<a class="gn-heading-anchor" href="#attraktiv-er-et-l%c3%b8fte" aria-label="Link til dette afsnit"><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>Der er en bagside, og jeg tror, det er den vigtige del.</p>
<p>At være den, alle vil have, hæver barren. Medlemmer, der sammenligner klubber, sammenligner også os, og en kiosk, der er frosset klokken halv seks om morgenen, er ikke bare en fejl. Det er øjeblikket, hvor nogen holder op med at spørge &ldquo;hvorfor ikke her?&rdquo;. Tiltrækningen holder kun, så længe produktet bliver ved med at gøre sig fortjent til den.</p>
<p>Det var det, sidste del af min kommentar handlede om. &ldquo;We will keep delivering&rdquo; lyder som sådan noget, man skriver under en kollegas opslag. Jeg mente det som et løfte. Salgsteamet kan kun lukke aftaler så hurtigt, fordi golfspillerne allerede kan lide det, de har brugt, og at det bliver ved med at være sådan, er mit job.</p>
<p>Det er et godt job at have. De fleste dage er jeg bare en udvikler, der retter et printlayout eller jagter en fejl. Men når golfspillere tager hjem og beder deres klub om det, vi laver, så indrømmer jeg det gerne: et øjeblik har jeg det som en hot chick. Og intet motiverer mere end at ville blive ved med at være det.</p>
]]></content:encoded>
      <category>Produkt</category>
      <category>Kultur</category>
    </item>
    <item>
      <title>Projektlederen træder ind</title>
      <link>https://build.golfnext.com/da/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/da/posts/enter-the-project-manager/</guid>
      <description>Nogen reviewede en af vores pull requests ordentligt, linje for linje, og godkendte den — mens forfatteren stadig pushede til branchen. Godkendelsen landede på en version, der ikke længere fandtes. Det er et symptom; sygdommen er, at et code review her kunne bestilles på fire forskellige korrekte måder og derfor nåede ingen. Om køer der ikke er køer, hvad kodeagenter gjorde ved vores evne til at reviewe, og hvorfor løsningen begynder med at slette et af de steder, vi spørger. Projektlederen træder ind.</description>
      <content:encoded><![CDATA[<p>Min uddannelse er i økonomistyring. Ikke datalogi.</p>
<p>Det er den indadvendte slags: ekstern regnskabsaflæggelse producerer de tal, omverdenen får at se, økonomistyring producerer de tal, de mennesker, der driver forretningen, har brug for for at kunne beslutte. Effektivitet, planlægning, budgettering, ledelse — fire ord, der ligner fire fag, og som i virkeligheden er ét spørgsmål stillet på fire måder. Hvor går en knap ressource hen, og hvordan ville man opdage, at den gik et sted hen, den ikke burde.</p>
<p>Ret det mod softwareleverance, og det ord, der først skifter form, er effektivitet. Det holder op med at betyde at arbejde hurtigere og begynder at betyde ikke at bruge den samme knappe ressource to gange. Og ressourcen er ikke penge, og det er ikke udviklertimer. Det er opmærksomhed — den vedholdende slags, der skal til for at læse en andens kode ordentligt.</p>
<p>Min titel er solutionarkitekt og teknisk projektleder. Folk spørger til den første halvdel, og den er den nemme: jeg tegner arkitekturen for de nye services, vi bygger, og gentegner de dele af det, vi allerede kører, som trænger til det. Det er arbejdet med tegninger til.</p>
<p>Ingen har nogensinde spurgt mig om noget som helst ved den anden halvdel, og det er den halvdel, der afgør, om den første nogensinde bliver leveret. En arkitektur, ingen kan finde opgaven til, få et review på eller høre om, når den er klar, er en tegning. Så: roadmappet, opgavestyring, GitHub, og hvordan folk finder ud af, at der er sket noget.</p>
<p>Jeg giver dommen først, for et indlæg, der holder den tilbage for at lyde afbalanceret, spilder din tid. Måden, vi bestiller og får code reviews på, er noget rod — intet enkelt sted svarer på spørgsmålet &ldquo;hvad venter på mig&rdquo;, og der findes ingen mekanisme i det overhovedet ud over, at folk husker. På et tidspunkt havde vi over hundrede pull requests åbne og markeret klar til review. Jeg har været med til at bygge det. Det virker ikke.</p>
<p>Og en økonom ville give den bunke et mindre mildt navn end backlog. Hundrede færdige, ikke-merged ændringer er <strong>varelager</strong>: arbejde, der allerede er betalt fuldt ud, som vi ikke har realiseret noget af, og som ligger på et lager og stille taber værdi, mens kodebasen bevæger sig videre under det. Det er det dyreste, afdelingen ejer, og intet af det står nogen steder i regnskabet.</p>
<h2 id="et-review-krævede-nogle-gange-at-jeg-rykkede-en-kollega">Et review krævede nogle gange, at jeg rykkede en kollega<a class="gn-heading-anchor" href="#et-review-kr%c3%a6vede-nogle-gange-at-jeg-rykkede-en-kollega" aria-label="Link til dette afsnit"><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>Ikke altid, og ikke alle. Men ofte nok til at lægge mærke til det, og ofte nok til at det blev en del af jobbet, jeg ikke havde regnet med at skulle lave.</p>
<p>En pull request er åben. Den er lille, den er klar, og den har ligget. Alt er, hvor det skal være: branchen er pushet, beskrivelsen fortæller hvad der er ændret og hvorfor, revieweren er sat på. Der sker ingenting, så går jeg hen og spørger. Nogle gange spørger jeg igen.</p>
<p>Nogle af vores har ligget i månedsvis. Ikke blokeret, ikke uenighed om, ikke ventende på en beslutning. Bare åbne, siden foråret.</p>
<p>Jeg vil være præcis omkring fejlen, for den oplagte forklaring er forkert. Ingen afviser noget. Ingen synes, ændringen er dårlig. Der er ikke uenighed nogen steder i det, og det er netop det, der gør det så tosset — det er ren friktion, som hverken producerer information eller beslutninger, kun forsinkelse.</p>
<h2 id="vi-gjorde-det-billigt-at-skrive-kode">Vi gjorde det billigt at skrive kode<a class="gn-heading-anchor" href="#vi-gjorde-det-billigt-at-skrive-kode" aria-label="Link til dette afsnit"><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>Vi bruger kodeagenter i stor stil, og denne blog har brugt flere indlæg på at være begejstret for dem. Her er regningen.</p>
<p>En agent gør det muligt for én person at have flere ting i luften ad gangen. Det er reelt, og det er det meste af produktivitetsgevinsten. Men arbejde i luften er ikke gratis. Hver tråd optager en plads i hovedet, og pladserne bliver ikke flere, bare fordi det går hurtigere at taste.</p>
<p>Så gennemløbet af at <em>producere</em> ændringer steg, og kapaciteten til at <em>reviewe</em> dem flyttede sig ikke en tøddel. Review er stadig ét menneske, der læser, i menneskefart, og det kræver præcis den vedholdende opmærksomhed, som parallelt arbejde er designet til at bryde op.</p>
<p>To ting følger, og de forstærker hinanden. Der kommer flere pull requests. Og de folk, der burde læse dem, holder hver især tre tråde af deres eget, hvilket er nøjagtig den tilstand, hvor en andens branch er nemmest at udskyde.</p>
<p>Jeg har <a href="/da/posts/road-to-glory/">skrevet her før</a>, at agenter har gjort rewrites billige at producere og ikke en krone billigere at gennemgå. Det er den samme sætning rettet mod almindeligt arbejde. Flaskehalsen forsvandt ikke, da vi blev hurtigere. Den flyttede sig til review, og vi flyttede ikke med.</p>
<h2 id="vi-har-aldrig-valgt">Vi har aldrig valgt<a class="gn-heading-anchor" href="#vi-har-aldrig-valgt" aria-label="Link til dette afsnit"><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>Den anden årsag har intet med AI at gøre og alt med os at gøre.</p>
<p>Vi har et opgavesystem. Vi har GitHub. Vi har en chatkanal, der er lavet specifikt til at lægge pull requests op, som ønskes reviewet. Alle tre er rigtige. Alle tre er i daglig brug. Og et review kan legitimt bestilles i hvilken som helst af dem — som en GitHub review request, som en kommentar på opgaven, som et opslag i kanalen, eller ved at vende sig om og sige det højt.</p>
<p>Fire ruter til én forespørgsel. Hver især forsvarlig; kombinationen uforsvarlig.</p>
<p>Det er værre end almindeligt kaos, for fra hver enkelt vinkel ser det fint ud. Hver forespørgsel røg et fornuftigt sted hen. Hvert system bliver brugt efter hensigten. Ingen gør noget forkert. Men intet enkelt sted rummer svaret på &ldquo;hvad venter på mig lige nu&rdquo;, så ingen har en kø. De har fire delvise, og at samle den rigtige kø koster mere end at læse selve pull requesten ville have gjort.</p>
<p>Det er hele fejlen, og det er ikke en værktøjsfejl. Hvert eneste af de systemer virker. Vi drukner i ikke at have valgt mellem dem.</p>
<h2 id="dørklokken">Dørklokken<a class="gn-heading-anchor" href="#d%c3%b8rklokken" aria-label="Link til dette afsnit"><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>Under ruteproblemet ligger et mindre og mere ondskabsfuldt et.</p>
<p><strong>En kommentar på en opgave notificerer ingen.</strong> Folk skriver en omhyggelig kommentar, på den rigtige opgave, med den rigtige detalje, og venter så. Der sker ingenting, for ingen fik det at vide. Kommentaren ligger præcis, hvor den hører hjemme, og præcis hvor ingen kigger.</p>
<p>Så sagde mine kolleger, ganske rimeligt: hvis jeg skal læse noget, så tag mig. Fair nok. En mention er det, der gør en besked til en notifikation — ikke beskeden, ikke kanalen, mentionen. Og der er en fælde indeni: ingen bliver notificeret om sin egen besked, så hvis du skriver på en andens vegne under dit eget navn, er den ene person, det med sikkerhed ikke når frem til, dig selv.</p>
<p>Jeg begyndte at tagge. Det hjalp. Det løste det ikke.</p>
<p>En notifikation er en dørklokke, og en dørklokke virker kun, hvis nogen er hjemme. Tag en person, der er tre tråde nede i sit eget arbejde, og du har leveret en afbrydelse, vedkommende bevidst udskyder — og en udskudt afbrydelse bliver ikke gemt nogen steder. Den er væk i det øjeblik, den næste ting ankommer.</p>
<p>Det ville være nemt at skrive det her som en klage over kolleger, der glemmer. Det er ikke det, der sker, og det er ikke rimeligt. <strong>Hvis en proces afhænger af, at folk husker, er processen allerede brudt sammen</strong> — det at huske er det bærende led, og menneskelig hukommelse er det forkerte materiale til det. Mine kolleger er ikke defekten. Et system, hvis eneste mekanisme er &ldquo;tag dem og håb&rdquo;, er defekten, og det system har jeg bygget.</p>
<h2 id="klar-til-review-betyder-ingenting">&ldquo;Klar til review&rdquo; betyder ingenting<a class="gn-heading-anchor" href="#klar-til-review-betyder-ingenting" aria-label="Link til dette afsnit"><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>Vend tilbage til det tal. Ikke hundrede pull requests åbne — hundrede markeret <em>klar</em>, hvilket er hundrede påstande om, at en ændring var færdig og ventede på nogen.</p>
<p>Og så det her. Nogen tog én af dem, reviewede den ordentligt, linje for linje, og godkendte den. Forfatterens reaktion var, at opgaven ikke var færdig: han arbejdede stadig på den og havde pushet til branchen, mens den blev læst.</p>
<p>Så godkendelsen landede på den forrige version. Revieweren brugte sin opmærksomhed på kode, der ikke længere fandtes, da han var færdig — og medmindre repositoryet afviser forældede godkendelser, når der kommer nye commits, sidder det grønne flueben nu på kode, ingen har læst. Værre end en ureviewet pull request, som i det mindste ser ureviewet ud.</p>
<p>Ingen brød en regel. Det er pointen: der var ingen. &ldquo;Klar til review&rdquo; var et mærkat, hvem som helst kunne sætte på når som helst, og som betød alt fra &ldquo;det her er færdigt, læs det venligst&rdquo; til &ldquo;denne branch findes&rdquo;. Et signal, der kan betyde to ting, betyder ingenting, og hundrede af dem betyder ingenting hundrede gange.</p>
<p>Det forklarer også månederne. En pull request, der har ligget åben siden foråret, er ikke en, nogen bliver ved med at beslutte at udskyde; ingen beslutter noget som helst om den. Den holdt op med at være en forespørgsel den dag, den ikke længere kunne skelnes fra de andre nioghalvfems — derefter er den en række i en liste og en branch, der stille ældes op mod en kodebase, der er kørt videre under den.</p>
<h2 id="hvad-jeg-gjorde-rigtigt-og-hvad-det-ikke-rørte-ved">Hvad jeg gjorde rigtigt, og hvad det ikke rørte ved<a class="gn-heading-anchor" href="#hvad-jeg-gjorde-rigtigt-og-hvad-det-ikke-r%c3%b8rte-ved" aria-label="Link til dette afsnit"><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>Alt, jeg har indført her, kommer af én sætning: <strong>hvert faktum findes i præcis ét artefakt.</strong></p>
<p>Hvad der er brug for og hvorfor bor i opgaven. Hvad der er ændret og begrundelsen bor i pull requesten. At der ønskes et review bor i selve forespørgslen. Hvorfor en ikke-oplagt linje ser ud, som den gør, bor i en kodekommentar.</p>
<p>Pointen er ikke ordentlighed, men omkostning. Det samme faktum fire steder koster den, der skriver det, og som taster det igen — og hver eneste læser bagefter, der nu har fire kilder, som var enige den dag de blev skrevet, og som har drevet fra hinanden lige siden. At gentage opgaven i pull requesten og pull requesten i chatbeskeden er den største enkeltkilde til oppustethed i det, vi producerer — og oppustethed er ikke et stilproblem. Det er grunden til, at ingen læser noget af det.</p>
<p>Der, hvor det har virket, er dokumenter. Opgavebeskrivelser har loft på fyrre linjer, struktureret som Problem, Forventet, Acceptkriterier. Pull request-beskrivelser har loft på 250 ord, og en triviel ændring får fem linjer og ingen overskrifter: ingen testtal, ingen &ldquo;CI grøn&rdquo;, ingen filoversigter, for CI melder sin egen status, og diffen viser sine egne filer. At skrive det ud igen producerer bare en mere forældet kopi af det, der allerede står på skærmen, og skubber det ene, kun forfatteren ved — <em>hvorfor</em> — længere ned på siden.</p>
<p>Min favorit, og den folk diskuterer: <strong>når din forståelse af en opgave ændrer sig, omskriver du beskrivelsen.</strong> Du tilføjer ikke. Ingen &ldquo;revideret&rdquo;, ingen &ldquo;erstatter&rdquo;, ingen dateret ændringslog inde i beskrivelsen. Det føles som at ødelægge beviser, og det er det ikke — værktøjet gemmer historikken, ligesom git gør. Dagbogsudgaven efterlader læseren med selv at regne ud, hvilket afsnit der stadig er sandt, hvilket er præcis det arbejde, beskrivelsen fandtes for at gøre for dem. En opgave siger, hvad der er sandt nu.</p>
<p>Den regel virker, fordi den styrer <em>hvor et faktum skrives ned</em>, og at skrive er en bevidst handling, en konvention kan gribe. Den har ikke det mindste tag i, hvor en forespørgsel bliver <em>besvaret</em>. En konvention kan fortælle dig, hvor du skal lægge noget. Den kan ikke få nogen til at samle det op.</p>
<p>Og det er den ærlige facon på mine første måneder her. Jeg skrev regler for artefakterne, fordi artefakterne er den del, en nyankommen kan ændre alene, og gik så ud fra, at den menneskelige halvdel ville følge dokumenterne ind i orden. Det gjorde den ikke. Dokumenterne er blevet klart bedre. Flowet omkring dem er det samme rod som før, og nu er det et rod med velformateret indhold.</p>
<h2 id="at-vælge-er-at-fjerne-noget">At vælge er at fjerne noget<a class="gn-heading-anchor" href="#at-v%c3%a6lge-er-at-fjerne-noget" aria-label="Link til dette afsnit"><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>At vælge lyder som at tilføje en regel. Det er det ikke. Hele bevægelsen går ud på at fjerne noget.</p>
<p><strong>Ét sted at spørge, og det er GitHub.</strong> Et review bestilles på pull requesten, med GitHubs egen review request. Det er hele rutebeslutningen.</p>
<p><strong>Ingen code review-kanal.</strong> Det er den del, der koster noget, for kanalen virker — folk skriver i den, det er en rimelig ting at gøre, og jeg er den, der har bedt kolleger om at bruge den. Den ryger alligevel. Et andet korrekt sted at spørge er det, der skabte problemet, og at beholde det, fordi det er harmløst i sig selv, er præcis sådan, vi endte med fire ruter. Man kan ikke halvvejs vælge.</p>
<p>GitHub vinder af grunde, der intet har at gøre med, at det er bedre software. Forespørgslen bor ved siden af det, der bliver spurgt om, så der er intet mellemrum mellem notifikationen og arbejdet. Dens tilstand overlever alt — en genstart, en ferie, en glemt mention. Og den afhænger ikke af, at nogen husker, hvilket er den egenskab, alle de andre muligheder manglede.</p>
<p><strong>Opgavesystemet beholder sit job, som er status, ikke forespørgsler.</strong> Opgaven bevæger sig gennem sine statusser, så tavlen er et ærligt billede af, hvor udviklingen faktisk er. Det er ikke der, man beder om et review; det er der, man kigger for at se, hvad der sker.</p>
<p><strong>Snart holder de to op med at blive opdateret i hånden.</strong> Når en pull request bliver merged, skal opgaven flytte fra code review til klar til release af sig selv. Det er ét-faktum-reglen rettet mod status i stedet for prosa: om noget er merged, er et faktum, GitHub allerede ejer, og hver manuel statusopdatering er en kopi nummer to af det, holdt i trit af et menneske, der har travlt. Kopier driver fra hinanden. En projektion kan ikke.</p>
<p><strong>Og så vanen: din pull request-indbakke.</strong> GitHub har én på <a href="https://github.com/pulls/inbox">github.com/pulls/inbox</a> — alt, der venter på dig, i én liste, med et tal på. Kig i den ofte, og specifikt <strong>kig i den, før du starter en ny opgave</strong>. Den placering gør det meste af arbejdet: den lægger review på det tidspunkt, hvor du alligevel er mellem to ting, i stedet for at bede dig afbryde dig selv — og den betyder, at køen bliver kortere, hver gang nogen starter noget nyt, i stedet for længere.</p>
<p><strong>Og en pull request forbliver et udkast, indtil forfatteren mener, den er klar til release.</strong> Ikke klar til at blive læst — klar til release, med ændringen allerede på staging og gennem accepttest. Udkast er standardtilstanden, og et udkast koster ingenting at åbne.</p>
<p>Det vender det, vi havde, på hovedet. &ldquo;Klar til review&rdquo; var standardtilstanden, og det er derfor, det ikke bar nogen information, og derfor nogen kunne godkende en version, forfatteren stadig arbejdede på. Gør &ldquo;klar&rdquo; til en bevidst handling, og det begynder at betyde noget igen — og &ldquo;pusher jeg stadig til den her?&rdquo; bliver forfatterens spørgsmål at besvare på forhånd i stedet for reviewerens at opdage bagefter.</p>
<p><strong>Én opgave, én pull request.</strong> Hvis en opgave kræver mere end én, er den ikke én opgave: bryd den op i delopgaver, og giv hver af dem en pull request. Jeg regner med mere diskussion om det her end om noget andet, og det er den, jeg vil forsvare hårdest, for det er egentlig ikke en regel om sammenkædning. Det er en mekanisme, der fremtvinger opdeling. I det øjeblik man er ved at åbne pull request nummer to på den samme opgave, har opgaven fortalt en, at den var for stor — og den fortæller det på det tidspunkt, hvor det stadig er billigt at dele den, i stedet for fire måneder senere, når nogen skal reviewe resultatet.</p>
<p>Det er også det, der gør automatiseringen mulig. &ldquo;Når en pull request bliver merged, flyttes opgaven til klar til release&rdquo; betyder kun noget, hvis en opgave har én pull request. Med tre er den tvetydig, nogen skal træffe afgørelsen, og så er vi tilbage ved et menneske, der holder to systemer i trit i hånden.</p>
<p>Til virkelig stort arbejde får forældreopgaven en feature branch, og delopgavernes pull requests peger på den branch i stedet for master. Forælderen merges én gang, når det hele er færdigt. Det giver ét sted at følge tilstanden på et stort stykke arbejde, uden at nogen lader som om, det nogensinde var én ændring.</p>
<p>Jeg skal være ærlig om modsætningen i den sidste. Jeg har <a href="/da/posts/in-the-jungle/">andetsteds argumenteret</a> for at samle os om én langlivet branch, og en feature branch er en langlivet branch. Den skelnen, jeg vil holde fast i, er varighed og ejerskab: denne her lever, så længe én forældreopgave lever, og bliver slettet med den, hvor de development-, staging- og production-brancher, vi er ved at afvikle, lever for evigt og stille samler divergens. Det er en afgrænset undtagelse snarere end en kovending — men det <em>er</em> en undtagelse, og hvis jeg om nogle måneder finder de her brancher stadig i live, tog jeg fejl.</p>
<p>Det svage punkt er værd at sige højt. To af de her er mekanismer, og én er en vane. GitHub som eneste rute kan håndhæves, udkast-som-standard næsten. &ldquo;Kig i din indbakke, før du starter noget&rdquo; er en vane — i et indlæg, der i høj grad handler om, at det sidste, jeg byggede på en vane, ikke holdt.</p>
<p>Forskellen er, tror jeg, at indbakken er en faktisk kø og ikke en bøn om at huske. Den har et tal, det er det samme tal i morgen, og den tømmer ikke stille sig selv, når din opmærksomhed flytter sig. Det er et bedre fundament end en chatkanal, man skal komme i tanke om at åbne. Men det er stadig en vane, og hvis den fejler, fejler den på samme måde.</p>
<h2 id="hvordan-et-stykke-arbejde-faktisk-ser-ud">Hvordan et stykke arbejde faktisk ser ud<a class="gn-heading-anchor" href="#hvordan-et-stykke-arbejde-faktisk-ser-ud" aria-label="Link til dette afsnit"><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>Der kommer en stor opgave, eller et lille projekt. Og så, nogenlunde i denne rækkefølge:</p>
<p>Jeg designer en løsning på det. Jeg bryder den op i opgaver og delopgaver. Jeg kontrollerer, at hver applikation, tingen rører ved, kan tale med de andre på den måde, designet forudsætter — og det er dér, de fleste ubehagelige overraskelser bor, for at to systemer er enige på et diagram, er ikke det samme som at to systemer er enige om en payload. Jeg kontrollerer, at det er sikkert, i de grænseflader hvor det er nemt at være doven. Og jeg bliver ved med at spørge, om resultatet stadig er noget værd for dem, der bad om det, for det er fuldt ud muligt at designe noget, der er korrekt, velopdelt, sikkert — og uden værdi for nogen.</p>
<p>Det er begge halvdele af titlen i ét stykke arbejde, og det er den ærlige grund til, at de står på samme visitkort. Designet er arkitekthalvdelen. Alt efter den første sætning er projektlederhalvdelen, og den er længere.</p>
<p>Jeg planlægger det meste af det sammen med Claude. Ikke for at få skrevet planen — for at forhøre den. Jeg lægger en tilgang ind og spørger, hvad der er galt med den, beder om de alternativer jeg ikke har tænkt på, spørger hvad hver af dem koster. Den er rigtig god til det, og den er hurtig nok til, at det at vurdere tre designs ikke længere er noget, man springer over, fordi man allerede har brugt en formiddag på det første.</p>
<p>Den har én fejltilstand, man aktivt skal styre: den er imødekommende. Skub en middelmådig idé frem med selvtillid, og du får ofte hjælp til at gøre den bedre frem for et kontant &ldquo;det her har den forkerte facon&rdquo;. Så når en beslutning betyder noget, eller når jeg er reelt i tvivl, tager jeg den videre til en anden og nogle gange en tredje kollega.</p>
<p>Det er ikke en formalitet. En model hjælper mig med at tænke, og den fortæller mig ikke, at jeg er den tredje i år, der foreslår det her, eller at sidste forsøg faldt på noget, ingen skrev ned. Institutionel hukommelse bor ikke i et kontekstvindue. Den bor i de mennesker, der var her.</p>
<h2 id="at-skære-i-tre-retninger">At skære i tre retninger<a class="gn-heading-anchor" href="#at-sk%c3%a6re-i-tre-retninger" aria-label="Link til dette afsnit"><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>Det andet initiativ er kedeligere at beskrive og betyder formentlig mere: projekter bliver brudt op i delprojekter, hver især små nok til rent faktisk at blive færdige, hver især med forretningen inde over undervejs frem for til sidst.</p>
<p>Grunden er ikke planlægningsæstetik. En bid, der tager fire måneder, producerer præcis ét øjeblik, hvor nogen uden for arbejdet kan reagere på det, og det øjeblik indtræffer, efter at alle de dyre beslutninger er truffet. Man får ikke feedback, man får en dom. Og den producerer også noget andet, som dette indlæg allerede har brugt to tusind ord på: en pull request, ingen kan reviewe. Bjerget er ikke kun et vaneproblem. Nogle af de brancher er simpelthen for store til at læse, og det var de allerede den dag, de blev åbnet.</p>
<p>Der er en anden grund, og den handler om mennesker snarere end feedback-sløjfer.</p>
<p>Planen er lang. Ét sprog bag det hele, den samme facon i hvert repository, to adminportaler der bliver til én — det måles i sæsoner, og det har jeg <a href="/da/posts/road-to-glory/">sagt offentligt</a>. Sagt i sin helhed motiverer sådan en plan ingen. Den er en mur. Og vi har en stribe af dem i kø, hver især værd at lave, hver især måneder dyb.</p>
<p>At bryde dem op gør dem ikke kortere. Månederne er stadig månederne. Det, der ændrer sig, er, hvor ofte man får bevis for, at tingen virker, og det viser sig at være dét, der afgør, om folk bliver ved. Et dusin projekter på flere måneder er en stemning. De samme tolv, hvor den første skive af hver bliver færdig inden for fjorten dage, er noget, man kan rumme i hovedet og blive ved med at gå efter — for man kan se det bevæge sig.</p>
<p>Det er undervurderet som grund til at planlægge ordentligt. Ikke for at planen er rigtig — det bliver den ikke — men for at afstanden mellem &ldquo;vi blev enige om at gøre det her&rdquo; og &ldquo;der skete noget&rdquo; er kort nok til, at folk bliver ved med at pege den vej. Det er prognose frem for planlægning: man bliver ikke bedre ved at forudsige hårdere, man bliver bedre ved at forkorte intervallet, før virkeligheden fortæller en noget. Forbeholdet er, at en velopdelt plan er nem at blive ved med at følge, efter den er holdt op med at være rigtig. Skiverne er små, så man kan skifte mening mellem dem, ikke så man slipper for at skulle.</p>
<p>Det er også den mindst synlige del af jobbet. Jeg hjælper vores produktchef med at bygge og vedligeholde roadmappet, og det meste af det arbejde handler ikke om at beslutte, hvad der skal stå på det — idéer er ikke den knappe ressource her — men om at beslutte, hvad en linje på det må betyde. En post, der ikke kan blive færdig inden for et kvartal, er ikke en plan, men et ønske med en dato på. At bryde de poster ned, indtil hver enkelt er noget, der faktisk kan afsluttes, og så blive ved dem til de gør det, er det meste af, hvad &ldquo;at sikre at vi når vores mål&rdquo; viser sig at bestå af. Det er uglamourøst, og det er forskellen på et roadmap, der beskriver året, og et der forudsiger det.</p>
<p>Det svære er, at en skive skal tilfredsstille tre publikummer på én gang.</p>
<p>Den skal være <strong>til gavn for brugerne</strong> — levere noget, en uden for teamet kan bruge, eller i det mindste se. Den skal være <strong>overkommelig for udviklerne</strong> — lille nok til at rumme i hovedet, reviewe i én ombæring og opgive uden sorg. Og den skal være <strong>fornuftig for forretningen</strong> — et skridt, nogen kan sige ja eller nej til, i termer der ikke handler om kode.</p>
<p>Rammer man ikke alle tre, gør opdelingen ingenting. Den fejl, jeg har set mest, er at skære efter udviklervenlige linjer, fordi de er nemmest at planlægge: alle datamodellerne, så alle endpoints, så alle skærmene. Fuldstændig fornuftig ingeniørrækkefølge — og den producerer måneders arbejde, som ingen udefra kan vurdere, fordi intet af det er en ting endnu. Man har delt arbejdet op uden at dele risikoen op.</p>
<p>To former, det tager hos os.</p>
<p><strong>Udgiv det til os selv først.</strong> Et nyt analyseværktøj går til GolfNext-ansatte alene, i en uge. Derefter udvides det, og det, det udvides til, formes af, hvad alle sagde. Denne blog har <a href="/da/posts/four-products-68-repos/">før indrømmet</a>, at vi ikke har nogen gradvis udrulningsmekanisme, og at vores første rigtige ring er geografi — nogle få nærliggende klubber og en genopretningsplan, der involverer tyve minutter i en bil. Ansatte-først er en ring, der ligger før den, koster ingenting og kunne findes i denne uge. Det er ikke en canary-deployment. Det er den billigst tænkelige måde at opdage, at noget åbenlyst er galt, før en klub gør det.</p>
<p><strong>Refaktorér ét modul ad gangen.</strong> Ikke hele systemet, og heller ikke en tilfældig skive: ét snit, hvor alle tre betingelser holder på én gang. Brugerne får noget, udviklerne får et stykke, de kan gøre færdigt, forretningen får et skridt, det er værd at finansiere. Det er den samme disciplin som sømme-før-rewrites-reglen, jeg har <a href="/da/posts/road-to-glory/">argumenteret for et andet sted</a>. Det nye er, at det <em>at finde snittet</em> er arbejdet. Det er ikke en planlægningsformalitet, man overstår, før det rigtige job begynder.</p>
<p>Jeg vil nødig oversælge det. At skære dårligt er værre end ikke at skære, for et dårligt snit leverer en halv feature og lærer alle, at inkrementel levering betyder ufærdige ting. Kunsten ligger i, hvor kniven sættes, og det tror jeg ikke, man kan skrive ned som en regel. Det er den del af dette job, jeg vil forsvare som noget, der faktisk kræver dømmekraft — hvilket er ubehageligt at indrømme i et indlæg, der ellers argumenterer for, at svaret er færre valg og flere mekanismer.</p>
<h2 id="spørgsmålet-der-ligger-under-det-hele">Spørgsmålet, der ligger under det hele<a class="gn-heading-anchor" href="#sp%c3%b8rgsm%c3%a5let-der-ligger-under-det-hele" aria-label="Link til dette afsnit"><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>Vi planlægger at ændre arkitekturen og stakken: ét sprog bag det hele, én facon i hvert repository. Det har jeg <a href="/da/posts/road-to-glory/">skrevet om et andet sted</a>. Det, der betyder noget her, er reaktionen. Mine kolleger er enige i, at det er en god idé. Ikke modvilligt. De synes, det er rigtigt.</p>
<p>Og der skete næsten ingenting i den retning, før <a href="/da/author/jb/">Johann</a> og jeg dukkede op.</p>
<p>Det gik mig på et stykke tid, og de dovne forklaringer er alle sammen forkerte. Det var ikke tilladelse — den her virksomhed siger ja til en idé på en dag, og jeg har fået idéer godkendt her hurtigere, end jeg kunne have booket et møde på mit forrige job. Det var ikke evner. Og det var bestemt ikke, at ingen havde tænkt på det: det er folk, der kender dette kodelandskab langt bedre end jeg gør, og jeg tør vædde på, at det meste af det, der står i vores plan, er blevet sagt højt, i en eller anden form, år før jeg kom.</p>
<p>Så hvorfor skete der ingenting?</p>
<p><strong>Fordi enighed ikke er en mekanisme.</strong> En ting, alle er enige om, og ingen ejer, har ingen måde at starte på. Der er ingen uenighed at afgøre, så der bliver ikke indkaldt til et møde. Der er ingen kunde, der venter, så den vinder aldrig en tirsdag over noget, der har en. Det hjælper ikke, at arbejdet lignede en udifferentieret klump på flere måneder uden tidlig gevinst, hvilket er præcis den facon, ingen melder sig frivilligt til — og præcis det, opdelingen er til for. Den forbliver ganske enkelt sand og ugjort, og det at være åbenlyst rigtig gør ingenting for at flytte den. Man kan se den samme effekt i hundrede pull requests, ingen er uenige i.</p>
<p><strong>Fordi der i det meste af virksomhedens historie ikke var nogen, hvis job var større end ét projekt.</strong> En stor del af vores software blev bygget af eksterne partnere, og de var dygtige. Men man engagerer en partner til at levere en ting. Ingen engagerer en partner til at ordne det, der ligger <em>mellem</em> tingene. Tværgående arbejde har ingen kunde, så under den model bliver det ikke bygget, og tretten års udgave af det samler sig i mellemrummene.</p>
<p><strong>Fordi de folk, der kunne have gjort det, er de samme, alt andet står i kø bag.</strong> Det er bus factor set fra en anden side: den, der kender et system bedst, er den, enhver forespørgsel om det venter på, så de har mindst luft, netop fordi de har mest viden. Man omstrukturerer ikke et kodelandskab indefra den position.</p>
<p><strong>Og fordi året har en facon.</strong> Golf er sæsonbetonet. Klubberne har travlt om sommeren, så vi har travlt om sommeren, og luften er om vinteren — hvilket gør vinduet for strukturelt arbejde smalt, årligt og nemt at bruge på noget mere højlydt.</p>
<p>Hvilket bringer mig til den ubehagelige del, og til grunden til anden halvdel af min titel.</p>
<p>Det, Johann og jeg kom med, var ikke bedre idéer. Det var en tom kø og lov til at spørge hvorfor. En nyankommen bærer ingen arvet backlog de første måneder, forventes at stille dumme spørgsmål og har endnu ikke internaliseret noget af det lokale &ldquo;sådan er det bare&rdquo;. Det er en reel ressource med kort holdbarhed; hver uge her bruger lidt af den.</p>
<p>Så projektlederhalvdelen er ikke der for at påtvinge proces på folk, der klarede sig fint uden. Den er der for at eje de ting, alle er enige i, og ingen har. Nogen skal omsætte enighed til en opgave med et navn på, og det viser sig at være et job snarere end en eftertanke.</p>
<p>Er noget af det sjovere? Dokumenterne er. En opgave, man kan læse på et minut, en pull request, der fortæller hvorfor i stedet for hvad, og betydeligt mindre gentastning af ting, der allerede stod skrevet et andet sted. Reviewflowet er ikke sjovere. Det er diagnosticeret, hvilket ikke er det samme, og hver eneste løsning ovenfor er højst fjorten dage gammel.</p>
<p>Og derfor er køen ubehagelig at skrive om. Det er den samme fejl som den, jeg lige har brugt fire afsnit på at forklare, i mit eget område, på min vagt.</p>
<p>Alle er enige om, at de hundrede pull requests bør reviewes.</p>
]]></content:encoded>
      <category>Ledelse</category>
      <category>Teknik</category>
    </item>
    <item>
      <title>En dag i Ry</title>
      <link>https://build.golfnext.com/da/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/da/posts/one-day-in-ry/</guid>
      <description>Otte ugers virkelige øjeblikke foldet ind i én dag på kontoret i Ry: en idé sagt højt over kaffen, et opkald til en klub, der ikke havde opdaget, at dens kiosk var nede, et dashboard, stifteren ikke kan gå forbi, spillere, der spørger efter vores enheder ved navn, og aftenen, hvor jeg sendte 94 kiosker ud i en genstartsløkke og lappede dem én for én.</description>
      <content:encoded><![CDATA[<p>Klokken er lidt over ni om aftenen, og jeg er koblet på en kiosk i en golfklub og lapper den i hånden. Når den er færdig, kobler jeg mig på den næste. Der er en del tilbage.</p>
<p>Det vender vi tilbage til. Jeg har været hos GolfNext i otte uger, og hver gang jeg prøvede at skrive om dem, endte jeg med en liste. Så i stedet har jeg foldet otte ugers virkelige øjeblikke ind i én enkelt dag på kontoret i Ry, fra den første kop kaffe til den sidste kiosk. Intet af det nedenfor skete på én dag. Men det hele skete.</p>
<h2 id="0740-hvilken-farve-er-det-i-dag">07:40. Hvilken farve er det i dag?<a class="gn-heading-anchor" href="#0740-hvilken-farve-er-det-i-dag" aria-label="Link til dette afsnit"><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>Inden jeg tager på arbejde, er der én beslutning at træffe, og den er for det meste truffet for mig. Vi har firma-t-shirts i forskellige farver, og tre dage om ugen har vi alle den samme på: blå mandag, grøn onsdag, sort fredag. Den her dag er foldet sammen af otte uger, så jeg kan ikke sige, hvilken trøje jeg har på. Måske alle tre. Under alle omstændigheder er det Black Friday hver uge hos GolfNext.</p>
<h2 id="0750-kaffe-og-en-idé-sagt-højt">07:50. Kaffe, og en idé sagt højt<a class="gn-heading-anchor" href="#0750-kaffe-og-en-id%c3%a9-sagt-h%c3%b8jt" aria-label="Link til dette afsnit"><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>Kaffemaskinen er der, hvor det meste starter her. Nogen siger &ldquo;hvad nu hvis vi bare&hellip;&rdquo; og inden frokost er der en branch. Nogle gange er en idé fra morgenen live inden for 24 timer. Ingen skal godkende, at du prøver noget. Code review og releaseprocessen gælder stadig, men afstanden mellem &ldquo;hvad nu hvis&rdquo; og &ldquo;det kører&rdquo; er kort, og det ved alle. <a href="/da/author/jn/">John</a> har skrevet et helt indlæg om det, <a href="/da/posts/culture-of-enthusiasm/">begejstringskulturen</a>, og han har ret.</p>
<p>Mit yndlingseksempel har et fjollet navn, og det må jeg godt sige, for jeg har selv fundet på det. <em>All you can Ball.</em> På en driving range køber man normalt en kurv bolde. Med All you can Ball køber man i stedet et tidsrum, og én kode giver bolde igen og igen, indtil tiden er gået. Hver portion er afmålt efter den tid, du har tilbage, så ingen tømmer maskinen i første minut og så står og ser tilfredse ud i niogtreds minutter.</p>
<p>Så, over endnu en kop kaffe, den næste tanke. Hvorfor overhovedet have kurvestørrelser? Hold fingeren på betalingsterminalen, og boldene løber, så længe du holder, som en tankpistol. Jeg kalder det ølglaseffekten: folk stopper, når det ser fuldt ud. Peter, der grundlagde firmaet i 2012, var vild med den. Det er stadig en idé. Idéer er billige her, og det er netop derfor, så mange af dem bliver til noget.</p>
<h2 id="0930-nogen-kommer-forbi">09:30. Nogen kommer forbi<a class="gn-heading-anchor" href="#0930-nogen-kommer-forbi" aria-label="Link til dette afsnit"><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>Det her er den del, jeg ikke havde regnet med ville betyde så meget, som den gør.</p>
<p>Hjalte kommer forbi. En PDF med boldkoder, som klubberne printer ud, fylder to sider. Ikke nogen krise. Den slags, der ligger en måned i backloggen, fordi den aldrig helt er det vigtigste.</p>
<p>John havde rettet det inden for en time.</p>
<p>En anden aften bliver medlemmer i en klub opkrævet gæstepriser. Jeg sætter mig sammen med en af vores eksterne udviklere, vi finder fejlen sammen, og den er rettet inden for en time. De eksterne kolleger byggede kioskerne, bookingflowene og betalingsintegrationerne og holdt dem kørende i mere end ti år, før nogen af os dukkede op. At have dem og os i samme samtale er det bedste fra begge verdener.</p>
<h2 id="1100-opkaldet">11:00. Opkaldet<a class="gn-heading-anchor" href="#1100-opkaldet" aria-label="Link til dette afsnit"><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>Sidst på formiddagen ser jeg, at en kiosk i en klub er gået offline. Jeg nævner det. Hjalte opretter ikke en sag. Han tager telefonen og ringer til klubben.</p>
<p>De havde ikke opdaget det endnu. De var glade for, at vi ringede.</p>
<p>Siden da har det været reglen: når vi ser noget galt, ringer vi. Det er en lille ting, og den virker, fordi den, der ser problemet, og den, der kender klubben, sidder i samme bygning.</p>
<h2 id="1200-længere-nede-ad-bordet">12:00. Længere nede ad bordet<a class="gn-heading-anchor" href="#1200-l%c3%a6ngere-nede-ad-bordet" aria-label="Link til dette afsnit"><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 startede i slutningen af august for at tage sig af den mobilapp, spillerne bruger. På hans første dag sagde jeg, at appen var hans: gør det, du synes er bedst, jeg kommer ikke til at gætte på dig bagefter. Han begyndte med den uglamourøse del: appens TypeScript-fejl fra 134 til nul, lint-fejlene fra 264 til nul, og automatiske tests, der kører på rigtige telefoner. Intet af det kan en golfspiller se. Alt det gør den næste funktion sikrere at sende ud.</p>
<h2 id="1300-skærmene-på-væggen">13:00. Skærmene på væggen<a class="gn-heading-anchor" href="#1300-sk%c3%a6rmene-p%c3%a5-v%c3%a6ggen" aria-label="Link til dette afsnit"><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>Efter frokost ender alle foran fjernsynene.</p>
<p>De kom for et stykke tid siden, leveret sammen med en andens palle kattemad, som blev sendt retur. En aften byggede jeg, uden at nogen havde bedt om det, et dashboard til dem med Claude. Seneste releases. Hvad der kører hvor, fordi supporten havde spurgt. Åbne pull requests. Et hjerteslag fra hver enhed derude. Bolde afgivet pr. klub i dag i forhold til samme dag sidste uge. Og et tilfældigt programmørmeme, for et dashboard uden en joke er bare et regneark på en væg.</p>
<p>Dagen efter blev Peter ved med at stoppe op foran det. Mest ved boldtallene, hvor han så klubberne overhale hinanden i løbet af dagen. En dag senere sendte vores CEO salgsslides, der skulle med. Det er blevet skærmen, vi står omkring med kaffen, og det kan jeg ikke sige om noget andet dashboard, jeg har bygget.</p>
<h2 id="1430-tvillingerne">14:30. Tvillingerne<a class="gn-heading-anchor" href="#1430-tvillingerne" aria-label="Link til dette afsnit"><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>Om eftermiddagen åbner John og jeg begge en pull request til den samme funktion: at vælge, hvilken version af softwaren en kiosk kører. Ingen af os havde sagt det til den anden. Samme idé, samme dag, to implementeringer.</p>
<p>Vi slog de to sammen. Det er ikke første gang, vi har gjort sådan noget. Vi kalder hinanden tvillingerne nu, fordi vi bliver ved med at ende samme sted fra hver sin retning. Hvis to mennesker, der ikke har talt sammen, havner samme sted, er det nok et godt sted.</p>
<p>Den funktion hører til det største, jeg har bygget her: fleet-servicen. Det er ét system, der kender hver eneste fysiske enhed, vi har derude, kioskskærme, betalingsterminaler, boldautomater, og styrer dem fra ét sted. En enhed melder sig ind én gang med en 12-cifret kode, der tastes ind på stedet, får sin egen identitet og henter derefter sine indstillinger og sin version fra servicen i stedet for, at nogen sidder og piller ved den i hånden. Vi tændte for det en nat i september, mens klubberne var lukket. Om morgenen havde alle klubbernes kiosker meldt sig ind. Den eneste, der holdt ud, var vores egen demoenhed i Sverige, som var blevet sat op to gange og klagede over, at en enhed med dens ID allerede fandtes. Selvfølgelig var det demoenheden.</p>
<p>Hjalte har et bedre navn for indmeldingsdelen. Han kalder det 1-Click-installeren. Ingen skal længere koble sig på en enhed og sætte dens miljø op i hånden. En 12-cifret kode, og enheden er vores. Sådan kommer vi til at sende ting ud fremover. Næste skridt, og vi er tæt på, er en lille webside, hvor personalet i en klub selv sætter en ny enhed op, og jeg har en fornemmelse af, at det bliver den sjove slags opsætning. Og at udskifte en defekt enhed kommer ikke til at kræve flere telefonopkald. Det kræver en pakke og et link.</p>
<h2 id="1500-en-andens-ur">15:00. En andens ur<a class="gn-heading-anchor" href="#1500-en-andens-ur" aria-label="Link til dette afsnit"><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>Midt på eftermiddagen en besked fra en klub i Portugal: deres boldautomat åbner cirka en time for sent, hver eneste morgen. Ingen havde rørt ved tidsplanen. Det viste sig, at automaten blandede sin egen tidszone sammen med Danmarks, og det gjorde alle automater uden for Danmark. Pengene havde samme vane. Priserne gik ud fra danske kroner med to decimaler, og det måtte laves om, før en klub i Storbritannien kunne tage imod pund, og før vores kiosk i Island overhovedet kunne gå live, fordi den islandske krone ikke har decimaler. Nu kommer tid og valuta fra klubben, ikke fra hvor vores kontor tilfældigvis ligger.</p>
<h2 id="1600-nyheden">16:00. Nyheden<a class="gn-heading-anchor" href="#1600-nyheden" aria-label="Link til dette afsnit"><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>Sidst på eftermiddagen fortæller en af vores mest erfarne kolleger holdet noget, de har hørt. Spillere er begyndt at gå til deres klubber og spørge, om de kan få GolfNexts enheder. Fordi de er så nemme at bruge. Fordi de er praktiske.</p>
<p>For mig ramte det hårdere end noget tal på væggen. Så meget af vores arbejde bliver kun bemærket, når noget går galt. Imens står en golfspiller et sted ved en disk og spørger efter det, vi laver.</p>
<h2 id="1800-dagen-der-blev-lang">18:00. Dagen, der blev lang<a class="gn-heading-anchor" href="#1800-dagen-der-blev-lang" aria-label="Link til dette afsnit"><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>Hvilket bringer os tilbage til aftenen. Den her del er fra min tredje uge.</p>
<p>Jeg havde jagtet en fejl, hvor kioskerne nogle gange frøs. Jeg ville have flere detaljer, så jeg fik kiosken til at holde en pause, før den lukkede, lige længe nok til at skrive en crashlog. Det, jeg ikke havde tænkt på, var, hvordan kioskerne opdaterer sig: opdateringen lukker appen, bytter filerne og venter kun så længe. Min pause gjorde appen for langsom til at lukke. Opdateringerne blev afbrudt igen og igen, og omkring 94 kiosker begyndte at genstarte i en løkke. Opdateringer kører kun, når en kiosk står ledig på sin startskærm, så ingen blev smidt ud midt i at købe en runde. Det var stadig 94 kiosker, der kørte i ring. Og crashloggen hjalp mig ikke engang med at finde frysningen.</p>
<p>Det akavede var, at jeg ikke bare kunne sende en rettelse ud. Den opdateringslogik, der kører, er den, der allerede ligger på kiosken, altså den samme, som var i stykker.</p>
<p>Det tog omkring to timer at stoppe blødningen ved at fastfryse den opdatering, kioskerne hentede. Så kom resten af aftenen, scenen fra starten af indlægget: koble sig på kioskerne én ad gangen og lappe dem i hånden, så den næste release ville holde.</p>
<p>Det gjorde den.</p>
<h2 id="0800-næste-morgen">08:00, næste morgen<a class="gn-heading-anchor" href="#0800-n%c3%a6ste-morgen" aria-label="Link til dette afsnit"><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>Jeg kom ind og var forberedt på jokes. Der kom ingen. Vores produktchef var ikke bekymret. Folk var glade for, at nogen havde lagt timerne.</p>
<p>Der er et tysk ordsprog, <em>wo gehobelt wird, fallen Späne</em>. Hvor der høvles, falder der spåner. Vi har høvlet en hel del på otte uger, omkring 650 mergede pull requests mellem John, Max og mig, og den aften var en af spånerne. Den efterlod også noget brugbart. Kioskerne giver nu op efter tre mislykkede opdateringsforsøg og beholder den version, de har. Og den aften er grunden til, at fleet-servicen ruller ting ud, som den gør: en ny version går først til nogle få kiosker, og når den virker, går den til resten. Hvis noget er galt, sættes en kiosk tilbage til sin tidligere version med en indstilling. Det er den del af aftenen, jeg er mest stolt af.</p>
<h2 id="hvorfor-bygningen-betyder-noget">Hvorfor bygningen betyder noget<a class="gn-heading-anchor" href="#hvorfor-bygningen-betyder-noget" aria-label="Link til dette afsnit"><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>Den sidste fredag i september gik John og jeg ud på parkeringspladsen sammen og var enige om, at det havde været en stor uge. &ldquo;Det er ret tydeligt, hvad vi bidrager med til firmaet.&rdquo;</p>
<p>Jeg har tænkt over hvorfor, og jeg ender hele tiden ved bygningen, eller rettere ved hvor korte afstandene i den er. PDF&rsquo;en, der bliver rettet inden for en time, fordi nogen kom forbi. Opkaldet, der sker, fordi den, der så problemet, sidder ved siden af den, der kender klubben. Funktionen, der blev bygget to gange, fordi to mennesker tænker ens, og slået sammen, fordi de sidder sammen. Den gode nyhed, der når hele holdet på én gang, i samme rum.</p>
<p>Det er også det, fredag morgen er til. I en halv time sidder vi alle sammen om ét bord, i sorte t-shirts, med rundstykker, og fortæller om vores uge. Kortere bliver afstandene ikke.</p>
<p>I morgen er der en ny kop kaffe, og nogen siger &ldquo;hvad nu hvis vi bare&hellip;&rdquo; Jeg glæder mig til at høre, hvad det er.</p>
]]></content:encoded>
      <category>Kultur</category>
      <category>Teknik</category>
    </item>
    <item>
      <title>Vores agenter drak al kaffen</title>
      <link>https://build.golfnext.com/da/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/da/posts/agents-drank-the-coffee/</guid>
      <description>Vores kodeagenter pusher, venter på CI, retter og pusher igen, hele dagen. Da testsuiten voksede til to en halv gang størrelsen på en uge, stod hver CI-kørsel atten minutter ved kaffemaskinen. Sådan fik vi det ned på omkring tre minutter for de samme penge, og hvad der stod tilbage og ventede, da maskinerne ikke længere kunne bruges som undskyldning.</description>
      <content:encoded><![CDATA[<p>Der findes en xkcd om den bedste undskyldning, en programmør nogensinde har haft, og det meste af min karriere var den en dokumentar. Man pushede en commit og gik ud i køkkenet. Man så kaffemaskinen male bønner. Man drev ud til rygerne, for det var dér, de rigtige beslutninger blev truffet, og så kom man tilbage. Kørte stadig. Buildet var den eneste pause, nogen havde lov til at tage.</p>
<figure><img src="/en/posts/agents-drank-the-coffee/xkcd-compiling.png"
			alt="To udviklere, der fægter på kontorstole. Chefen spørger, hvorfor de ikke arbejder; de råber Compiling!"><figcaption>
			<p><a href="https://xkcd.com/303/">xkcd #303, “Compiling”</a> af Randall Munroe, <a href="https://creativecommons.org/licenses/by-nc/2.5/">CC BY-NC 2.5</a></p>
		</figcaption>
</figure>

<p>Du kender følelsen. Du har en rettelse. Den er lille, den er klar, den mangler bare et grønt flueben. CI siger tyve minutter. Så du gør det fornuftige og går i gang med noget andet, og når kørslen er færdig, er du dybt inde i det. Du kigger efter frokost. Den er rød: en flaky test eller en konflikt med noget, der er blevet merget i mellemtiden. Du retter det, pusher, venter igen og vender tilbage til det i morgen. En ændring på en time har taget to dage, og næsten ingen af den tid gik til selve ændringen. Alt, hvad du ville, var at komme igennem checks og videre.</p>
<p>I år fik buildet nyt selskab ved kaffemaskinen: vores kodeagenter. I september ventede de atten minutter på hver eneste kørsel. Her er, hvordan det blev til omkring tre, for de samme penge, og hvad vi fandt for enden af køen, da maskinerne ikke længere kunne bruges som undskyldning: os.</p>
<h2 id="hvem-der-venter-på-ci-nu">Hvem der venter på CI nu<a class="gn-heading-anchor" href="#hvem-der-venter-p%c3%a5-ci-nu" aria-label="Link til dette afsnit"><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>Det meste af vores team arbejder nu med kodeagenter, især Claude. En typisk udvikler her har fire eller flere agent-sessioner kørende på samme tid.</p>
<p>Hver af de sessioner arbejder i en løkke. Den skriver ændringen, pusher den, venter på CI (continuous integration: robotten, der bygger og tester hver ændring), læser, hvad der fejlede, retter det og pusher igen. Først når checks er grønne, reviewer et menneske den, med <a href="https://www.coderabbit.ai">CodeRabbit</a> som et ekstra sæt øjne, og et menneske merger den og beslutter, hvornår den når produktion.</p>
<p>Det forvandler CI fra noget, et menneske tjekker en eller to gange i timen, til noget, hver agent på teamet venter på, hele dagen. På GitHub Actions gik vi fra 48 kørsler per arbejdsdag i juni til 375 i september. Mergede pull requests gik fra omkring 140 om måneden i foråret til 583 i september. GitHub fakturerer per minut, og regningen gik fra omkring 29 dollar i juni til 135 dollar for september, med et par dage endnu tilbage.</p>
<p>Så blev vores primære webapplikation langsom, fordi vi havde skrevet en masse tests. I én uge i september, mens vi byggede et nyt admin-API, voksede suiten fra omkring 3.900 tests til omkring 9.600, de fleste af dem tests, der starter hele applikationen og taler med en rigtig database. En typisk pipeline-kørsel gik fra under seks minutter til atten, og en dårlig tog syvogtyve. Jobs sad ikke fast i en kø. Maskinen havde to CPU&rsquo;er, to en halv gang så meget arbejde, og vi havde lige bedt den om at køre otte testprocesser på én gang.</p>
<p>En pull request her får typisk to CI-kørsler, før den er færdig, og en fjerdedel får fire eller flere, mest opfølgende pushes frem for fejl. Ved atten minutter per kørsel er det over en halv time per ændring, før et menneske overhovedet kigger på den, og med fire eller flere sessioner per udvikler står mindst fire ændringer altid ved kaffemaskinen.</p>
<h2 id="ikke-en-vps">Ikke en VPS<a class="gn-heading-anchor" href="#ikke-en-vps" aria-label="Link til dette afsnit"><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>En kollega var den første, der sagde det højt: det her gør os alle sammen langsommere. Forslaget var en VPS, en lejet virtuel server. Lej en stor en, installér GitHubs runner-software (den agent, der samler CI-jobs op) på den, færdig. Det ville have virket.</p>
<p>Jeg var alligevel ikke vild med det. En selvhostet runner er et kæledyr. Nogen skal patche den, holde Docker sund på den, installere de rigtige sprog og versioner til hvert repository, der bruger den, tømme disken, når et build fylder den, og opdage det, når den er nede en lørdag. Jeg vil hellere have, at ingen på et team af vores størrelse bruger deres uge på det. Så jeg ledte efter noget hostet i stedet, men først rettede jeg testene.</p>
<h2 id="først-testene-så-maskinerne">Først testene, så maskinerne<a class="gn-heading-anchor" href="#f%c3%b8rst-testene-s%c3%a5-maskinerne" aria-label="Link til dette afsnit"><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>Den første rettelse var almindeligt ingeniørarbejde, og den stod for det meste. Testene kørte allerede parallelt, men med en masse spild omkring dem: hver test-worker byggede sin egen kopi af databasen, databasen gemte omhyggeligt hver skrivning på disk, som om det betød noget, og den samme kode blev kompileret igen i hver proces. Så nu ligger testdatabasen i hukommelsen, den bygges én gang og kopieres til hver worker, og suiten er delt op i fire jobs, der kører samtidig. Opdelingen er bevidst dum: tæl testene i hver fil, vægt de langsomme typer tungere, og del filerne ud, tungeste først, til det job, der har mindst indtil videre. De fire jobs bliver ikke helt færdige på samme tid, og vægte baseret på faktiske tider er det oplagte næste skridt.</p>
<p>Databaseopsætningen gik fra 76 sekunder til under 30, testene brugte halvt så meget regnekraft, og på GitHubs standardmaskiner var pipelinen nede på omkring fem minutter. Men hvert af de fire jobs betaler nu for sin egen opsætning, så de fakturerede minutter per kørsel blev liggende på omkring 21, det samme som den langsomme pipeline havde kostet. Vi ventede omkring en fjerdedel så længe for den samme regning.</p>
<p>Samme dag, som testrettelserne landede, oprettede jeg en prøveperiode hos <a href="https://www.blacksmith.sh">Blacksmith</a>, den hostede løsning med den nemmeste opsætning. Blacksmith sælger det samme som GitHub, en maskine, der kører dine GitHub Actions-jobs, bortset fra at man kan vælge større og hurtigere. Man installerer ingenting. Man ændrer én linje per job:</p>
<pre tabindex="0"><code>runs-on: blacksmith-4vcpu-ubuntu-2404   # was: ubuntu-latest
</code></pre><p>Lige efter tilmeldingen bad jeg Blacksmith om at køre vores jobs i deres EU-region. CI bør kun nogensinde se testdata, aldrig en klubs eller en golfspillers. Skulle nogen komme til at træde forkert, står maskinerne i EU alligevel. De skiftede det inden for en dag. Der ligger ingen aftale bag dette indlæg: vi tog prøveperioden og betaler nu normalpris. Vil vi nogensinde væk igen, er det den samme ene linje tilbage.</p>
<p>Med de fire testjobs på Blacksmith har pipelinen kørt 62 gange siden. Medianen er 2,8 minutter, og ni ud af ti kørsler bliver færdige på under 3,7.</p>
<p>Tuning og opdeling tog en typisk kørsel fra atten minutter til omkring fem. Maskinerne tog den fra fem til under tre, og en del af det er simpelthen flere kerner, fordi to CPU&rsquo;er blev til fire. Jeg testede ikke GitHubs egen runner med fire kerner, som findes og koster 0,012 dollar i minuttet, det dobbelte af prisen for to CPU&rsquo;er, så noget af gevinsten er penge, enhver GitHub-kunde kunne vælge at bruge.</p>
<h2 id="hvad-der-ændrede-sig-og-hvad-der-ikke-gjorde">Hvad der ændrede sig, og hvad der ikke gjorde<a class="gn-heading-anchor" href="#hvad-der-%c3%a6ndrede-sig-og-hvad-der-ikke-gjorde" aria-label="Link til dette afsnit"><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>Jeg forventede, at hele løkken ville falde sammen. Så jeg målte det og sammenlignede pull requests fra den langsomme uge med de fem dage siden skiftet.</p>
<p>Den del, CI styrer, faldt faktisk sammen. Fra en pull request blev åbnet til dens første grønne kørsel tog det en median på 17,5 minutter i den langsomme uge. Nu er det 3,1. Det er ventetiden, hver ændring betaler, før review, og den er fem gange kortere.</p>
<p>Den del, jeg troede ville følge med, gjorde ikke. Når en kørsel fejler, er tiden til den næste grønne en median på omkring en halv time, før og efter (kun en håndfuld tilfælde hver uge, så tag det med et gran salt). Hurtigere maskiner kunne ikke røre ved den. Færre end én pull request ud af ti bliver rød, og de få, der gør, ligger for det meste, indtil et menneske tager fat i dem, nogle gange først næste morgen. Agenterne giver sjældent CI noget, der er i stykker. Den langsomme del af vores løkke er nu os.</p>
<h2 id="hvad-det-koster-og-to-lærestreger">Hvad det koster, og to lærestreger<a class="gn-heading-anchor" href="#hvad-det-koster-og-to-l%c3%a6restreger" aria-label="Link til dette afsnit"><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>Én pipeline-kørsel af den primære webapplikation plejede at fakturere omkring 21 minutter på GitHubs maskine med to CPU&rsquo;er, cirka 13 cent. På Blacksmith fakturerer den samme kørsel omkring 16 minutter, de fleste på de større maskiner, og lander på 12 eller 13 cent, alt efter hvordan man runder. De samme penge altså, for en kørsel på under tre minutter i stedet for fem, eller atten før vi rørte testene.</p>
<p>Blacksmith for september indtil videre: 12,68 dollar i forbrug, hvoraf de 12 dækkes af den gratis kvote. Vi skylder dem 68 cent.</p>
<p><strong>Opdeling køber tid, og du betaler stadig for hvert minut.</strong> Hvert job betaler for sin egen opsætning: at tjekke koden ud, installere afhængigheder, starte databasen. GitHub fakturerer hvert job per påbegyndt minut, så ti jobs på ét minut koster ti minutter og føles som ét. Det er det værd for en pull request, nogen venter på. For et natligt job, ingen holder øje med, så hold det samlet.</p>
<p><strong>Kig på ARM tidligt.</strong> Hos Blacksmith koster et ARM-minut over en tredjedel mindre end et almindeligt Intel- eller AMD-minut (x64), og at flytte CI derover ændrer ikke på det, man leverer. Vi har kun prøvet det på ét repository indtil videre, og det passer ikke alle steder: vores Android-app må blive på x64, fordi Googles Android-byggeværktøjer ikke kører på Linux ARM.</p>
<h2 id="at-flytte-resten">At flytte resten<a class="gn-heading-anchor" href="#at-flytte-resten" aria-label="Link til dette afsnit"><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>Vi har næsten halvtreds aktive repositories, og jeg vil ikke have, at hver migrering afhænger af, at nogen forstår netop den pipeline. Så jeg skrev processen ned som en skill til Claude, en drejebog, den følger trin for trin, når den flytter et repository over. Den måler før og efter, over flere kørsler frem for én heldig. Den tjekker, at caches faktisk bliver gendannet; en af vores var aldrig blevet det én eneste gang, og ingen havde lagt mærke til det. Og den prøver ARM, hvor intet udelukker det.</p>
<p>Fem repositories kører på Blacksmith indtil videre. Et startede der. Tre, heriblandt den primære webapplikation, kom over med Blacksmiths egen migreringsguide plus lidt hjælp fra mig, og et blev flyttet fra ende til anden af skill&rsquo;en. De tidlige tal for de andre ser gode ud, fra en smule hurtigere til omtrent dobbelt så hurtigt, men de hviler på en håndfuld kørsler hver.</p>
<h2 id="efterlad-pipelinen-hurtigere-end-du-fandt-den">Efterlad pipelinen hurtigere, end du fandt den<a class="gn-heading-anchor" href="#efterlad-pipelinen-hurtigere-end-du-fandt-den" aria-label="Link til dette afsnit"><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>Ingen holder fest, når CI virker. Når det er langsomt eller flaky, er alle hamrende irriterede. Det er en større del af jobbet, end vi har lyst til at indrømme, og det bliver langsomt, uden at nogen har besluttet det: nogle få tests ad gangen, indtil det en uge tager atten minutter, og alle stille og roligt er begyndt at arbejde uden om det.</p>
<p>Så jeg behandler det, som jeg behandler kode, med spejderreglen: efterlad pipelinen bedre, end du fandt den. Når jeg arbejder i et repository, kigger jeg på, hvor lang tid dets CI tager, og om dets caches virker, og hvis noget er åbenlyst langsomt, retter jeg det, mens jeg alligevel er der.</p>
<p>Med en agent koster det næsten ingenting. Jeg peger en mod repositoryet og lader den læse de tidligere builds, finde de langsomme trin, prøve en ændring, måle den, beholde det, der blev hurtigere, droppe det, der blev værre, og tage en runde til. Den kører i baggrunden, mens jeg arbejder på fejl og features, og den tager næsten ingen af min opmærksomhed. Skill&rsquo;en skriver den løkke ned. Jeg vil gerne have, at det bliver alles vane, ikke kun min.</p>
<h2 id="tilbage-til-kaffen">Tilbage til kaffen<a class="gn-heading-anchor" href="#tilbage-til-kaffen" aria-label="Link til dette afsnit"><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>Testsuiten vokser stadig, så en dag bliver pipelinen langsom igen. Med lidt held retter den, der opdager det, det, før kaffen er klar. Et sted, går jeg ud fra, træffer rygerne stadig de rigtige beslutninger. Jeg kan bare ikke længere bruge buildet som undskyldning for at slutte mig til dem.</p>
]]></content:encoded>
      <category>Teknik</category>
      <category>Infrastruktur</category>
      <category>AI</category>
    </item>
    <item>
      <title>99 % begejstring, 1 % bekymring</title>
      <link>https://build.golfnext.com/da/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/da/posts/culture-of-enthusiasm/</guid>
      <description>Jeg så en film en lørdag aften, mens en agent skrev en af vores frontends om fra Vue til React. Et par dage senere var den live. Det interessante er ikke AI&#39;en — det er, hvad der skal til i en virksomhed, før det overhovedet må ske, den ene bekymring ledelsen faktisk rejste, og hvorfor et miljø, der siger ja så hurtigt, kræver mere disciplin, ikke mindre.</description>
      <content:encoded><![CDATA[<p>Jeg sad og så en film en lørdag aften med min laptop åben på bordet ved siden af mig. Med jævne mellemrum kiggede jeg over, læste et par linjer og vendte tilbage til filmen. <a href="https://code.claude.com/docs">Claude Code</a>, Anthropics terminalbaserede kodeagent, var i gang med at skrive hele frontenden i en af vores applikationer om fra Vue til React.</p>
<p>Ikke en komponent. Hele frontenden. Hver enkelt skærm applikationen har, hvilket er titusindvis af linjer.</p>
<p>Jeg havde givet den en snæver opgave og snævre rettigheder: byg et proof of concept, i sit eget hjørne, uden at røre noget, der betød noget. Ingen havde bedt mig om det. Det var noget, jeg ville vide svaret på.</p>
<p>Mandag viste jeg det til <a href="/da/author/jb/">vores tech lead</a>. Han blev oprigtigt begejstret, tog proof of conceptet ind i det rigtige repository, brugte en runde på at rydde op i fundamentet og sendte det i produktion. I løbet af en dag eller to blev den gamle frontend slukket.</p>
<p>GolfNext, hvor jeg arbejder, bygger den software, golfklubber kører på: booking og medlemskaber, selvbetjeningskioskerne der fungerer som reception, når ingen står bag skranken, og betalingsdelen ude på driving rangen, altså træningsbanen, hvor man betaler for en spand bolde og slår dem af. Jeg havde været der fire uger. Den applikation, jeg skrev om, er en, klubpersonalet sidder i hele dagen — så når den går ned, står der en på en klub ved en skranke med en kunde foran sig.</p>
<p>Før det tilbragte jeg år i en væsentligt større virksomhed, og det er stort set hele pointen.</p>
<p>En virksomhed lod en medarbejder med fire uger på kontoen erstatte en frontend, som personalet bruger, med sit weekendeksperiment — inden for en uge. Det er den del, jeg stadig synes er overraskende, og AI&rsquo;en er det mindste af det.</p>
<h2 id="hvorfor-skrive-noget-om-overhovedet">Hvorfor skrive noget om overhovedet<a class="gn-heading-anchor" href="#hvorfor-skrive-noget-om-overhovedet" aria-label="Link til dette afsnit"><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>Eftersom resten af dette indlæg bruger en del plads på, hvorfor gode idéer går i stå andre steder, er det kun rimeligt at lægge begrundelsen for denne her frem med det samme.</p>
<p>Det er ikke, fordi React er bedre end Vue. Vores mobilapps er allerede React Native, og hvis den desktopapplikation vi leverer på et tidspunkt flytter over på en webflade, ville React være det ene framework bag hver eneste frontend i GolfNext, web som desktop. Ét sæt idiomer i stedet for to, ét dependency-træ og ét svar på et spørgsmål frem for to, der hver især kun passer halvt.</p>
<p>Den anden grund er mindre flatterende at tale om og betyder mere i det daglige: kodeagenter er mærkbart bedre til React end til Vue. Der findes langt mere React i verden, så langt mere af det er gået ind i træningen. Hvis vi skal bygge software på den måde, vi bygger den nu, er der et argument for at stå der, hvor værktøjerne er stærkest.</p>
<h2 id="hvad-der-var-sket-på-mit-forrige-job">Hvad der var sket på mit forrige job<a class="gn-heading-anchor" href="#hvad-der-var-sket-p%c3%a5-mit-forrige-job" aria-label="Link til dette afsnit"><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>Ingenting, eller tæt på. Men ikke af den grund, man nok ville gætte på.</p>
<p>Mit forrige job var en stor virksomhed, og jeg vil være fair over for den: den var ikke ved at drukne i bureaukrati. Der var ikke et vildnis af udvalg, en idé skulle igennem. Hvad der til gengæld var, var et lille antal seniorudviklere, som havde været der meget længe, sad på de fleste beslutninger og var godt tilfredse med tingene, som de allerede var.</p>
<p>Ingenting behøvede at blive formelt afvist, og for det meste blev ingenting det. Et forslag skulle bare undlade at vække interesse hos de to eller tre personer, hvis opbakning det krævede, og så skete det stille og roligt ikke. Det er en hårdere mur end en proces, for en proces fortæller dig i det mindste, hvad det næste skridt er.</p>
<p>Deployment-historien havde samme form. Releases var i høj grad manuelle, og evnen til at lave en — sammen med den adgang, man skulle have for at kunne undersøge noget, når det gik skævt — lå hos en håndfuld mennesker. Var man ikke en af dem, stoppede ens arbejde det sted, hvor det krævede noget, kun de kunne gøre, og så ventede det. Ikke af ond vilje. Det var sådan, rettighederne havde lagt sig gennem årene, og ingen, hvis dag det gjorde lettere, havde nogen grund til at ændre det.</p>
<p>Så den ærlige version af kontrasten handler ikke om hastighed. Den handler om, hvor meget af det, man allerede kan, man faktisk har lov til at gøre. Hos GolfNext var den, der gennemgik mit weekendeksperiment, også den, der kunne sende det i produktion. Der hvor jeg arbejdede før, ville det have været to forskellige personer og en kø.</p>
<h2 id="hvordan-en-idé-får-et-ja-her">Hvordan en idé får et ja her<a class="gn-heading-anchor" href="#hvordan-en-id%c3%a9-f%c3%a5r-et-ja-her" aria-label="Link til dette afsnit"><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>To veje, og ingen af dem har et godkendelsestrin.</p>
<p>Den første er en samtale. Nogen siger tingen højt, de der er i rummet er enige om, at den er værd at lave, og så bliver den lavet. Intet dokument. Den anden er en opgave i vores tracker, som er det samme med en titel og en ansvarlig, så den ikke bliver glemt.</p>
<p>Der er ingen tredje vej, hvor idéen tager af sted for at blive evalueret. Værd at være præcis omkring, hvad det betyder og ikke betyder: den port, vi ikke har, er den der afgør, om en idé <em>må prøves</em>. Code review og releaseprocessen ligger præcis, hvor man ville forvente.</p>
<p>De fleste af idéerne kommer fra vores stifter, som startede virksomheden i 2012. Han har flere af dem på en uge, end teamet kan bygge på en måned, hvilket nemt kunne være udmattende og for det meste ikke er det, fordi strømmen går i begge retninger, og en idé fra en udvikler får samme modtagelse som hans. Jeg har budt ind med flere, siden jeg startede, og ikke én af dem fik den behandling, jeg havde spændt op til.</p>
<p>Vores tech lead skrev noget, der lå tæt på det her, på denne blog to uger inde i sit eget job — han var mødt op med paraderne oppe efter modstand og fandt næsten ingen. Fire uger inde, fra en anden stol, vil jeg sige det samme. Forskellen er, at jeg nu har en historie, der er konkret nok at være uenig i.</p>
<p>Modtagelsen er heller ikke bare en godkendelse, og det er den del, jeg ikke havde regnet med. Vis noget til vores stifter, vores produktchef eller vores direktør, og inden for et minut sidder en af dem og bygger videre på det højt: hvad der kunne ligge oven på det, hvad det ellers kunne bruges til, hvor værdien for virksomheden i virkeligheden ville lande. Man kommer ind med én ting, man har lavet, og går ud med tre ting, den kunne blive til. Det er en god fornemmelse, og det er også en grund til at møde forberedt op: en idé her vokser i det øjeblik, man viser den, så det betaler sig at have tænkt over, hvor den kunne føre hen, før nogen anden gør det.</p>
<p>Hastigheden er den del, jeg stadig ikke har vænnet mig til. Nogle gange er en idé fra formiddagen live inden for fireogtyve timer. Andre gange tager det to uger. Det er sjældent et kvartal.</p>
<h2 id="den-del-jeg-ikke-vil-løbe-let-hen-over">Den del, jeg ikke vil løbe let hen over<a class="gn-heading-anchor" href="#den-del-jeg-ikke-vil-l%c3%b8be-let-hen-over" aria-label="Link til dette afsnit"><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;En AI skrev en frontend, som personalet bruger, om, og vi sendte den i produktion to dage senere&rdquo; er enten godt ingeniørarbejde eller en historie, der ender skidt.</p>
<p>Så her er detaljerne.</p>
<p>Agenten kunne oprette, og det var det hele: ingen ændring eller sletning af noget, der allerede fandtes, og alt af det ude i sit eget hjørne. Intet af det, den lavede, var én merge fra en golfklubs mandag morgen.</p>
<p>Derefter læste et menneske det igennem. Vores tech lead klikkede sig gennem hvert enkelt view — der er ikke så mange, tyve eller tredive med nogle formularer og modaler, en halv times klikkeri — og konklusionen var, at det så godt nok ud til at erstatte den gamle applikation uden at ødelægge noget. Hvad han ikke havde, var års historie med kodebasen; han havde været der omtrent lige så længe som mig. Så reviewet var ikke institutionel hukommelse. Det var nogen, der ved, hvordan en sund React-kodebase ser ud, der læste denne igennem, før den kom i nærheden af en klub.</p>
<p>Der, hvor han til gengæld brugte tiden, var fundamentet frem for opførslen: at få udtrykket tættere på den applikation, vi erstattede, få Biome ind, få styr på hvordan mapper og filer navngives, og at skille de steder ad, hvor agenten havde stablet halvtreds Tailwind-klasser på en enkelt div — omstruktureret, og derefter en linting-regel, så det ikke kan komme igen. Et menneske skal fortsat kunne læse tingen. Det er det arbejde, der gør det forsvarligt at bygge funktionalitet oven på, i stedet for at sætte næste etage på noget, der allerede læner sig som tårnet i Pisa.</p>
<p>Det review er den egentlige sikkerhedsmekanisme i den her historie, ikke modellen og ikke min prompt. Og ikke testene, for denne frontend har aldrig haft nogen særlig testsuite. Det var allerede sandt før omskrivningen. Omskrivningen gjorde det bare vigtigere.</p>
<p>Det var også kun frontend. Samme API, samme data, intet rørt på betalingsvejen.</p>
<p>Hvad vi ikke har, er en gradvis udrulning, og det har denne blog allerede indrømmet: vores kunder er fysiske steder, så den første ring er geografi, og hvis en release går skævt på en klub i nærheden, er redningsplanen tyve minutter i bil. Det er et reelt sikkerhedsnet, og jeg vil ikke kalde det en canary-deployment.</p>
<p>Det, jeg stadig bærer rundt på, er, at ingen har læst hver linje af den kode, som man læser kode, man selv har tastet. En omskrivning, der ser færdig ud, er det mest flatterende output en agent laver, for hullerne er udeladelser snarere end fejl, og en udeladelse dukker ikke op i en demo. Jeg regner med at blive ved med at finde dem et stykke tid endnu. Indtil videre er der ikke gået noget i stykker ude på en klub.</p>
<h2 id="planen-weekenden-hørte-til">Planen, weekenden hørte til<a class="gn-heading-anchor" href="#planen-weekenden-h%c3%b8rte-til" aria-label="Link til dette afsnit"><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>Ingen havde bedt om den omskrivning. Det var den første del af noget længere, jeg havde skitseret, og det der afgjorde timingen var banalt: jeg kiggede på, hvor meget af min Claude Code-kvote der var tilbage for måneden. Ubrugte tokens bliver ikke overført. Kapacitet, jeg ikke havde brugt, var kapacitet GolfNext allerede havde betalt for, og at bruge den kostede virksomheden ingenting.</p>
<p>Så jeg brugte den. Hen over den periode startede jeg proofs of concept for de fleste af vores applikationer, alle i ét sprog med én kodestruktur, for at finde ud af, hvordan en samlet stack faktisk ville se ud i stedet for at diskutere det i det abstrakte. Præcis én af dem, React-frontenden, er nået i produktion. Resten er stadig proofs of concept, og det vil jeg hellere sige lige ud end lade det her læse som en migrering, der allerede er sket.</p>
<p>Vi kører to administrationsportaler, hver med sin egen backend og sit eget login. De tjener forskellige formål, men der er ingen god grund til, at de er separate applikationer, og jeg ved oprigtigt ikke, hvorfor de blev bygget sådan — det ligger år før min tid. Hvad jeg ved, er, at alle udviklere her og alle i ledelsen er enige om, at der kun bør være én. Prisen for at have to lander på dem, der bruger dem: klubpersonalet logger ind to gange.</p>
<p>Planen er at ende med én. Først en auth-bro, så ét sæt loginoplysninger virker på tværs af begge. Dernæst selve sammenlægningen: én frontend og én navigation, med begge backends stadig kørende bagved. Først derefter backenden, hvor de to services erstattes af én, så der er ét sprog og én kodestruktur under hele portalen i stedet for to af hver.</p>
<p>Der er en grund til, at den vej er værd at gå nu frem for om to år. Ambitionsniveauet her er ikke beskedent. Vi er i atten lande i dag, planen omfatter en hel del flere, og en hel del flere brugere bag dem. Meget af det kommer gennem integrationer: hver klub kører allerede et eller andet administrationssystem, og vores position er at arbejde sammen med det, de har, frem for at bede dem om at smide det ud. Hver enkelt af dem er endnu et system at tale med og endnu et sæt antagelser at holde styr på.</p>
<p>To portaler, to backends og to autentificeringsordninger er til at leve med i den størrelse, vi har nu. Det er præcis den slags, der holder op med at være til at leve med, når det går godt. Så argumentet for at lave det ucharmerende strukturelle arbejde er, at der stadig er luft til at lave det, og det er også derfor, &ldquo;lad os samle stacken&rdquo; bliver mødt med begejstring her frem for et krav om at retfærdiggøre det. Alle ved godt, hvad der er på vej.</p>
<p>Det er en lang vej snarere end en sprint, og det meste af den er ikke sket endnu. At få frontenden over på React var forarbejdet til det hele, det der skulle være på plads, før nogen af de trin kunne begynde, og derfor var det netop det eksperiment, jeg valgte en lørdag aften. Trin to var at vise hele planen til de mennesker, der skulle leve i den.</p>
<h2 id="hvor-de-1--blev-af">Hvor de 1 % blev af<a class="gn-heading-anchor" href="#hvor-de-1--blev-af" aria-label="Link til dette afsnit"><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>Vores tech lead trak i samme retning og ville have resten af vores frontends over på React. Ledelsens reaktion på hele pakken — portalerne, backenden, frontendene — var, i runde tal, 99 % begejstring og 1 % bekymring.</p>
<p>Jeg vil være præcis omkring den 1 %, for den var ikke, hvad jeg havde forventet. Jeg havde spændt op til omkostninger eller risiko for kunderne. Hvad de var bekymrede for, var, hvordan de nuværende udviklere ville have det med det.</p>
<p>Det er mennesker, der har bygget denne software i årevis, i en stack de selv har valgt, og to af os — begge omkring en måned inde ad døren — foreslog at ændre den. Det er den rigtige ting at være bekymret for, og det er den bekymring, der aldrig optræder i et estimat.</p>
<p>Så vi holdt en brainstorm med teamet og holdt vores egne holdninger tilbage, indtil alle andre havde talt.</p>
<p>Det er sværere, end det lyder. Vi havde allerede vores forslag skrevet ned. Hver gang nogen bevægede sig i retning af det, var det fristende at hoppe ind og give dem ret, hvilket ville have afsluttet diskussionen og gjort alt derefter til samtykke i stedet for input.</p>
<p>Da vi endelig lagde vores på bordet, var der ingen, der var uenige. Sammenlægningen af portalerne og det, der følger efter, var de decideret begejstrede for. Selve React-standardiseringen var de bare fine med, og det er den mest interessante halvdel af sætningen: det er den del, der erstatter den stack, de selv havde valgt, og &ldquo;fine med&rdquo; forekommer mig en helt rimelig ting at føle omkring det. Ingenting i forslaget blev ændret som følge af mødet, hvilket er den mindst tilfredsstillende måde, et møde kan gå godt på, og det vil jeg hellere fortælle end pynte på. Vi arbejder i den retning nu.</p>
<p>Begejstring fra ledelsen er nem at komme til her. Det er en feature, og det er det meste af grunden til, at de fireogtyve timers omløbstid overhovedet findes. Begejstring fra de mennesker, der skal leve i koden hver dag, er den, man skal gøre sig fortjent til, og den eneste pålidelige måde, jeg kender til at gøre det, er at holde sin mund i den første halvdel af mødet.</p>
<h2 id="begejstring-kræver-disciplin">Begejstring kræver disciplin<a class="gn-heading-anchor" href="#begejstring-kr%c3%a6ver-disciplin" aria-label="Link til dette afsnit"><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>Jeg vil ikke efterlade det her som et indlæg om, hvor rart det er hos os, så lad mig sige, hvad miljøet faktisk kræver, for det er ikke ingenting.</p>
<p>Et sted, der siger ja inden for fireogtyve timer, virker kun, hvis det, man lægger frem, fortjener et ja inden for fireogtyve timer. Ingen af forslagene i dette indlæg blev skitseret i det møde, hvor de blev præsenteret. Vores tech lead og jeg har brugt mange timer sammen på planer og arkitektur og gennemgået dem grundigt, før nogen andre så dem, og hver især læste vi op på det først. De fleste idéer, jeg har bragt frem her, var undersøgt, før de blev præsenteret — ikke undervejs i diskussionen.</p>
<p>Weekendens proof of concept er det tydeligste eksempel. Formålet var ikke at producere kode. Det var at tage den mest risikable antagelse i en plan og få den afklaret, før nogen blev bedt om at binde sig til planen. Det er forberedelse forklædt som en hobby.</p>
<p>Det er den del, jeg gerne vil understrege for enhver, der tror, at en begejstret kultur lyder behagelig. Når der ikke er nogen gatekeeper, du skal overbevise, er der heller ingen, der kontrollerer din tankegang for dig. Den grundighed, en proces ville have påtvunget dig, skal komme fra dig selv i stedet, og den skal være der før mødet frem for efter. Begejstring sænker ikke barren for professionalisme. Den flytter barren over på dig.</p>
<p>Og det gør det her til en særlig slags arbejdsplads snarere end en entydigt bedre en. Er man ambitiøs og kan man godt lide at lave forarbejdet ordentligt, er det det bedste miljø, jeg har arbejdet i — hvad man kan nå, er afgrænset af ens egen tænkning frem for af nogens godkendelseskø. Vil man hellere have en proces, der fortæller, hvad der er det næste, og et udvalg til at dele vægten af at have taget fejl, tror jeg, man ville synes, det var ubehageligt.</p>
<p>Hvis du overvejer et sted som det her: det hurtigste ved GolfNext er ikke værktøjerne eller agenterne. Det er afstanden mellem at få en idé og at få lov til at prøve den.</p>
<p>Man skal godt nok passe på, hvad man tænker på en lørdag aften. Det kan være i produktion tirsdag.</p>
]]></content:encoded>
      <category>Kultur</category>
      <category>Teknik</category>
      <category>AI</category>
    </item>
    <item>
      <title>Min bror fra en anden mor?</title>
      <link>https://build.golfnext.com/da/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/da/posts/brother-from-another-mother/</guid>
      <description>Jeg startede i et job, hvor den anden halvdel af den tekniske side var begyndt dagen før mig, med en titel ved siden af min og ingen streg tegnet mellem dem. En fremmed, lige så ny som mig, og os to tilsyneladende sat til at styre det samme team. Hvad jeg egentlig var bekymret for, og hvad en måned har og ikke har afgjort.</description>
      <content:encoded><![CDATA[<p>Jeg var til samtale om det job, <a href="/da/author/jb/">Johann</a> har.</p>
<p>Tech Lead hos GolfNext — den virksomhed, der bygger den software, golfklubber kører på: booking af en runde, kassen i proshoppen, maskinerne ude på træningsbanen. Det har den gjort i over et årti, primært med udviklere der arbejdede på afstand, og det team, der har holdt det kørende, har været her betydeligt længere end nogen af os to.</p>
<p>Jeg gik gennem processen, og jeg ville have rollen. Tilbagemeldingen kom som to ting på én gang: de gik videre med en anden, og de ville oprette en ny rolle til mig, fordi de havde været imponerede nok til ikke at lade det stoppe dér. Det er ikke et afslag, man får særlig ofte.</p>
<p>Så den mand, der fik det job, jeg havde søgt, blev den mand, jeg skulle arbejde side om side med. Han startede mandagen. Jeg startede tirsdagen — én dag bagefter, hvilket er tæt nok på ingenting.</p>
<p>Hans titel er Tech Lead. Min er solutionarkitekt og teknisk projektleder. Læs de to op, og prøv at sige, hvor den ene stopper og den anden begynder. Ingen havde tegnet den streg, og så vidt jeg kan se, var der ingen planer om at gøre det.</p>
<p>Så jeg mødte op til et job, jeg oprigtigt glædede mig til, og inde i glæden sad en knude, jeg ikke sagde højt i et stykke tid.</p>
<p>På vores anden dag med at arbejde sammen sagde Johann, at vi var som tvillinger. Han har sagt det mange gange siden, og det er blevet en løbende joke mellem os — titlen på dette indlæg er min halvdel af den. Men jeg vil være ærlig om det, der kom før jokén.</p>
<h2 id="hvad-jeg-egentlig-var-bekymret-for">Hvad jeg egentlig var bekymret for<a class="gn-heading-anchor" href="#hvad-jeg-egentlig-var-bekymret-for" aria-label="Link til dette afsnit"><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>Fire spørgsmål, i den rækkefølge de blev ved med at komme.</p>
<p><strong>Ville vi være enige?</strong> Ikke om tabs og spaces. Om de ting, man ikke kan gøre to gange: hvilken stack vi samler os om, om man skriver om eller rydder op, hvad der bliver bygget først, når alt har et krav på at være først.</p>
<p><strong>Kunne vi overhovedet arbejde sammen?</strong> Jeg havde aldrig mødt ham. Ikke en tidligere kollega, ikke en vens ven, ikke nogen hvis arbejde jeg havde set. En fremmed med et tilstødende mandat og samme startmåned.</p>
<p><strong>Hvad sker der første gang, vi er fundamentalt uenige?</strong> To mennesker med overlappende ansvar og ingen grænse imellem går fint, så længe de er enige. Det er et møntkast den dag, de ikke er, og der er ingen proces at appellere til, for der er ingen proces.</p>
<p><strong>Og det, jeg ikke sagde højt: var en af os meningen at skulle sige op?</strong> Vi havde været kandidater til den samme stol få uger tidligere, og den ene af os havde ikke fået den. Virksomheder hyrer af og til to seniorprofiler ind i tilstødende roller og lader situationen løse sig selv. Ingen fortalte mig, at det var det, der foregik. Ingen fortalte mig heller, at det ikke var, og i uge ét har man ikke standing til at spørge.</p>
<h2 id="hvorfor-det-ikke-var-paranoia">Hvorfor det ikke var paranoia<a class="gn-heading-anchor" href="#hvorfor-det-ikke-var-paranoia" aria-label="Link til dette afsnit"><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>To samtidige senioransættelser i et lille team er et usædvanligt træk, og det er ikke oplagt et godt et. Teamet havde allerede mennesker, der havde vedligeholdt denne software i årevis. Nu havde det også to nye, ankommet med en dags mellemrum, som begge ville få meninger om den tekniske retning — og som ingen af dem havde optjent en enkelt times troværdighed inde i huset.</p>
<p>Jeg kom også fra et sted, der lærte mig, hvad et uafklaret spørgsmål om, hvem der bestemmer, gør ved en kodebase over tid. Jeg har set beslutninger samle sig omkring den, der har været der længst, og jeg har set fuldt ud gode idéer stille og roligt undlade at ske, fordi de aldrig vakte interesse hos de rigtige to personer. Når jeg ankom til et sted, hvor det at bestemme var udefineret mellem mig og en mand, jeg aldrig havde mødt, var mit instinkt ikke optimisme. Det var: dette kan gå skidt, og det kan gå skidt langsomt, hvilket er værre.</p>
<p>Én ting talte til min fordel, og det havde jeg ikke regnet med. Han havde allerede udgivet <a href="/da/posts/letter-day-one/">et brev til sig selv</a> på denne blog, før sin egen første arbejdsdag, hvor han lagde frem, hvad han gik ind til, og hvilke mål han satte sig. Jeg har læst det siden. At læse et andet menneskes erklærede kompas, før man har haft en enkelt rigtig samtale med ham, er en mærkelig slags forspring — man ved, hvad han siger, han vil være, og man får lov at se, om det holder. Én linje i det gør det meste af arbejdet: <em>hyr fremragende mennesker, og stol så på dem.</em> Man kan ikke skrive det offentligt og derefter bruge sin første måned på at samle territorium.</p>
<h2 id="en-måned-inde">En måned inde<a class="gn-heading-anchor" href="#en-m%c3%a5ned-inde" aria-label="Link til dette afsnit"><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>Vi har brugt en stor del af den seneste måned foran de samme problemer: hvad dette landskab faktisk består af, hvad der skal være sandt, før noget andet kan ske, hvad det første rigtige træk bør være.</p>
<p>Den ubehagelige del er, at det bliver ved med at gå på samme måde. Han beskriver formen på et problem, og det er den form, jeg selv ville have tegnet. Jeg kommer med det, jeg mener det første skridt må være, og det er det skridt, han allerede havde valgt. To mennesker, der uafhængigt af hinanden danner det samme billede af en virksomhed, ingen af dem kendte for en måned siden, er antingen et meget godt tegn eller en fælles blind vinkel, og indefra ser de to ting fuldstændig ens ud.</p>
<p>Det gjorde også troværdighedsspørgsmålet mindre på en måde, jeg ikke havde forudset. Én ny med en mening om retningen er én ny med en mening. To nye, der ankom hver for sig og nåede den samme konklusion, er trods alt et argument.</p>
<p>Så den ærlige rapport fra den første måned er, at det, jeg havde spændt op til, ikke er sket, og at enigheden har været tæt nok på til at være en anelse mistænkelig.</p>
<h2 id="hvad-der-sker-i-stedet-for-uenighed">Hvad der sker i stedet for uenighed<a class="gn-heading-anchor" href="#hvad-der-sker-i-stedet-for-uenighed" aria-label="Link til dette afsnit"><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 løbet af få dage sad vi og spurgte hinanden ud om præferencer for mere eller mindre alting. Værktøjer. Branching. Hvor meget proces et lille team skal bære. Hvornår man skriver noget om, og hvornår man lader det være. Hvordan et godt pull request ser ud. Hvordan man behandler et landskab, man ikke selv har bygget. Svarene blev ved med at passe sammen — ikke sådan cirka, men næsten hver gang.</p>
<p>Og når vi rent faktisk arbejder på noget, er det ikke et sammenstød. Den ene af os kaster en idé frem, den anden kommer tilbage med et forslag, der gør den bedre, og den version er den, vi tager. Jeg ved oprigtigt ikke, om det tæller som at være uenige, og jeg har besluttet ikke at gå op i, hvad det hedder, for det, der kommer ud på den anden side, er bedre end det, nogen af os kom ind med.</p>
<p>Jeg vil ikke påstå, at det afgør det svære tilfælde. En måned er ikke lang tid, og jeg kan ikke rapportere fra en konflikt, jeg ikke har haft. Men det ville være en overkorrektion at beskrive det, der er sket indtil nu, som konflikt, der blot er udskudt. Det ser ud som to mennesker, der opgraderer hinandens arbejde, og det er noget helt andet.</p>
<p>Det, jeg ikke havde taget med i regnestykket, er, hvor meget lettere jobbet er med en kollega, der er præcis lige så ambitiøs som en selv og peger i samme retning. Jeg havde spændt op til en rival. Hvad jeg har, er den person, jeg tjekker min tænkning af med, før den forlader huset.</p>
<h2 id="forskellige-vinkler-samme-mål">Forskellige vinkler, samme mål<a class="gn-heading-anchor" href="#forskellige-vinkler-samme-m%c3%a5l" aria-label="Link til dette afsnit"><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>Det overlap, jeg var bekymret for, er heller ikke opstået — og ikke fordi nogen forhandlede det væk. Vi satte os aldrig ned og delte territoriet. Det delte sig selv, efter angrebsvinkel.</p>
<p>I praksis betyder det, at vi typisk er rettet mod det samme problem fra forskellige sider. Den ene af os er dybt inde i sikkerhed, mens den anden skriver arkitekturen om til et stort projekt. Den ene tager en stor ny funktion på betalingssiden, mens den anden er nede i en hardwareintegration. Samme destination, to tilgange, og næsten ingen flade, hvor vi kommer i vejen for hinanden.</p>
<p>Det, der overrasker mig, er, hvor lidt koordinering det kræver. Der er ingen fast sync, hvor vi skærer ugen ud imellem os. Vi ved hver især nogenlunde, hvad den anden laver, og — vigtigere — hvad han laver det <em>for</em>, så opdelingen falder ud af arbejdet i stedet for ud af et møde. Jeg ville have forudsagt det modsatte: to mennesker med udefinerede ansvarsområder burde have brug for konstant koordinering for at holde sig ud af hinandens vej.</p>
<p>Og det er, tror jeg, det, der holder tvillingejokén i live, uden at nogen af os vedligeholder den. Vi bliver ved med at nå frem til den samme konklusion fra modsatte retninger.</p>
<h2 id="var-en-af-os-meningen-at-skulle-sige-op">Var en af os meningen at skulle sige op?<a class="gn-heading-anchor" href="#var-en-af-os-meningen-at-skulle-sige-op" aria-label="Link til dette afsnit"><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>Nej. Og jo længere jeg ser på, hvordan det blev håndteret, jo mere mener jeg, at ledelsen traf det rigtige valg.</p>
<p>To mennesker kom ud af den proces. I stedet for at tage den ene og sende den anden hjem tog de begge og byggede en rolle til den anden — hvilket kun giver mening, hvis man er ærlig om størrelsen af det, der skal gøres. Der er flere repositories i denne virksomhed, end der er uger på et år, fire produkter der betjener virkelige anlæg, og et årti af teknologibeslutninger, som alle var rimelige, da de blev taget, og som ikke går op i ét sammenhængende landskab i dag. Hver del af det har brug for nogen med tid til at tænke over det, og jeg er ikke én gang på en måned stødt på mangel på ting at eje.</p>
<p>Så det, der udefra lignede to mennesker i konkurrence om én stol, viste sig at være en virksomhed, der besluttede, at den havde brug for to. Den knude, jeg ankom med, kom af at læse det på den første måde. En måned inde er den del, jeg ville have kaldt risikoen — os to — den del, der fungerer bedst.</p>
<h2 id="så-bror-fra-en-anden-mor">Så: bror fra en anden mor?<a class="gn-heading-anchor" href="#s%c3%a5-bror-fra-en-anden-mor" aria-label="Link til dette afsnit"><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>Foreløbig ja — med spørgsmålstegnet efterladt, hvor det er, med vilje.</p>
<p>En måned, hvor det viste sig, at vi tænker på samme måde om en virksomhed, vi begge lige har mødt, er oprigtigt den bedste faglige overraskelse, jeg har haft i årevis. Det er også kun en måned. Det, der virkelig ville gøre sig fortjent til udtrykket, er den første beslutning, kun én af os kan træffe, håndteret godt, med teamet som publikum. Spørg mig igen bagefter.</p>
<p>Hvad jeg vil sige til enhver, der går ind til den samme opsætning: bekymringen er rimelig og værd at sige højt tidligere, end jeg gjorde. To seniorprofiler uden en grænse imellem sig er en reel risiko, og <em>vi kan godt sammen</em> er ikke en plan for den. Men den fejlsituation, jeg havde spændt op til — to nye, der stille og roligt konkurrerer om samme grund — har brug for en mangel at leve af. På et sted med så meget ugjort er der ingen.</p>
<h2 id="én-uenighed-for-ordens-skyld">Én uenighed, for ordens skyld<a class="gn-heading-anchor" href="#%c3%a9n-uenighed-for-ordens-skyld" aria-label="Link til dette afsnit"><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>Jeg bør korrigere én ting, for præcisionens skyld. Der er præcis én ting, vi er uenige om.</p>
<p>Vores opgavestyring og vores wiki. Min holdning er, at de er under middel. Johanns er, at de er komplet forfærdelige. Vi har været igennem det mere end én gang, fra modsatte retninger som sædvanligt, og vi ender på samme destination — noget må ændre sig — ad vejen om en ægte og indtil videre uafklaret uenighed om, hvor meget foragt der er berettiget undervejs.</p>
<p>Så det står fast. To nyankomne, én utegnet grænse, en måned med at være enige om arkitektur, proces, omskrivninger, branching og hvad der kendetegner en god pull request. Og én åben strid, om adjektiver.</p>
]]></content:encoded>
      <category>Kultur</category>
    </item>
    <item>
      <title>Slet opsætningsguiden</title>
      <link>https://build.golfnext.com/da/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/da/posts/delete-the-setup-guide/</guid>
      <description>Ethvert repository udleverer instruktioner, ingen holder ved lige, og en .env.example, som nogen skal udfylde ud fra en Slack-besked. To værktøjer — mise til værktøjskæden, fnox til hemmelighederne — flytter begge opgaver ud af README&#39;en og ind i repoet. Med et ærligt regnskab over, hvad det koster, og hvad det ikke løser.</description>
      <content:encoded><![CDATA[<p>Tænk på sidste gang du klonede et repository, du aldrig havde arbejdet i før.</p>
<p>Du læste README&rsquo;en. Den bad dig installere en JDK, og du havde den forkerte, så du gik ud og fandt den rigtige. Den bad dig installere Node, og versionsfilen sagde 18, mens lockfilen tydeligvis ville have 20. Et sted nede i bunden stod der <code>cp .env.example .env</code> og så, med mindre skrift, <em>udfyld værdierne</em>. Udfyld dem hvorfra? Fra en kollega. Så du spurgte på Slack, nogen indsatte en blok <code>KEY=value</code>-linjer til dig, og nu ligger et produktionsnært login i en chathistorik for altid: søgbart, backet op, eksporteret til hvilket compliance-værktøj virksomheden nu købte sidste år.</p>
<p>Ingen har sat sig for at bygge det. Det er, hvad der sker, når instruktionerne bor i et dokument i stedet for i repoet.</p>
<p>Jeg er tech lead hos GolfNext, en dansk virksomhed der bygger software til golfklubber, og mit første indlæg på denne blog var en opgørelse over, hvad jeg var trådt ind i. Otteogtres repositories, fire produktlinjer, tretten års akkumulerede teknologivalg: Java og Maven og Spring, Kotlin og Gradle, Node, Python, PHP, JavaFX, React Native. At komme i gang i et af dem fra kold start tog mellem to og seks timer afhængigt af, hvilket man valgte, og jeg satte mig selv et mål om fem minutter fra klon til kørende kode.</p>
<p>Det her er det første seriøse forsøg på det. To værktøjer, begge fra samme person, jdx. <a href="https://mise.jdx.dev/">mise</a> lægger de rigtige værktøjer på din PATH, <a href="https://fnox.jdx.dev/">fnox</a> lægger de rigtige hemmeligheder i dit miljø, og det er de to halvdele af ét problem. mise er kedeligt og bredt anvendt. fnox er ingen af delene endnu, og det vender jeg tilbage til. Det interessante er, hvad der falder væk, når begge er på plads, og hvad det gør ved de kodeagenter, jeg har kørende i de her repositories hele dagen.</p>
<p>Det er <em>mise</em> som i <em>mise en place</em>, det fra køkkenet. Udtales &ldquo;meez&rdquo;, /miːz/, én stavelse, uden <em>-uh</em> til sidst. Det vil du gerne have styr på, inden du siger det højt til et møde. fnox er nemmere: Fort Knox med de fleste bogstaver banket af, altså &ldquo;eff-nox&rdquo;, og navnet fortæller dig, hvad det er til.</p>
<h2 id="halvdel-et-værktøjerne">Halvdel et: værktøjerne<a class="gn-heading-anchor" href="#halvdel-et-v%c3%a6rkt%c3%b8jerne" aria-label="Link til dette afsnit"><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>Har du brugt nvm eller pyenv, kender du formen: erklær en version, <code>cd</code> ind, få den version. Forskellen er, at mise er ligeglad med, hvad versionen er <em>af</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>Det er en JVM, et build-værktøj, en JavaScript-runtime, en Python og en hemmelighedsscanner (<a href="https://github.com/gitleaks/gitleaks">gitleaks</a>) i én fil, installeret med én kommando, på din PATH kun inde i dette katalogtræ. Tilføj <code>mise activate</code> til din shell-profil, og selv kommandoen forsvinder: <code>cd</code> ind i repoet, og værktøjerne er der, <code>cd</code> ud, og det er de ikke. Den sætter også de miljøvariable, der hører til, så <code>JAVA_HOME</code> peger på den JDK, dette projekt har fastlåst, i stedet for hvad dit styresystem sidst besluttede.</p>
<p>&ldquo;Versionsstyring&rdquo; underspiller det her, og jeg vil gerne være konkret om hvorfor. Det dyre ved en polyglot kodebase er skiftet, ikke den enkelte teknologi. Du bruger en formiddag i en Spring-service, hopper så til et Android-repo, så et lille Python-værktøj, og hvert hop er en chance for, at din shell er forkert på en måde, der giver en forvirrende fejl frem for en tydelig. I et af vores repositories fejler en nyere system-JDK ikke højlydt. Den ødelægger et mocking-bibliotek under test, adskillige lag nede, med en besked der intet har med Java-versioner at gøre. Det er en time af nogens liv, og du betaler den igen, hver gang en ny person starter, eller en computer bliver skiftet ud.</p>
<p>Et sprogspecifikt værktøj kan ikke hjælpe, fordi problemet ligger <em>mellem</em> sprogene.</p>
<p>En detalje, du lige så godt kan stjæle som opdage selv. Vi lader bevidst nogle af versionerne flyde: <code>latest</code> til scanneren, <code>lts</code> til Node. Hvad et <em>build</em> er lavet af, er fastlåst i CI-workflowet, og det er den fastlåsning, dependency-automatikken holder øje med og laver pull requests på. Fastlåste vi de samme versioner i <code>.mise.toml</code> også, ville vi have to steder at holde i sync, og det ene ville stille og roligt sakke bagud. Det ved vi, fordi det allerede var sket: en <code>.mise.toml</code> der påstod Node 22, mens alle fire workflows sagde 24. Kun én fil får lov at være den autoritative, og det er workflowet.</p>
<p>JDK&rsquo;en er undtagelsen, fastlåst hårdt begge steder med vilje. En forkert major-version dér er præcis den mocking-fejl ovenfor, og jeg holder hellere to filer i sync end genopdager den fejl klokken fire om eftermiddagen. Alt andet flyder.</p>
<h2 id="den-del-jeg-ikke-havde-regnet-med-at-holde-af">Den del, jeg ikke havde regnet med at holde af<a class="gn-heading-anchor" href="#den-del-jeg-ikke-havde-regnet-med-at-holde-af" aria-label="Link til dette afsnit"><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>Ethvert repository har en håndfuld ting, man gør ved det: bygger det, tester det, kører det, formaterer det. I et Maven-projekt er det <code>mvn spring-boot:run -Dspring-boot.run.profiles=local</code>. I et Gradle-projekt er det <code>./gradlew</code> og et task-navn med fire ord klistret sammen. I et Node-projekt er det, hvad <code>scripts</code>-blokken nu siger i denne uge. Intet af det er svært. Alt af det er noget, man skal slå op, når man er i et ukendt repo, hvilket er præcis dér, hvor man har mindst tålmodighed til at slå noget op.</p>
<p>mise lader dig lægge de udsagnsord i samme fil som værktøjerne:</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>Så bliver grænsefladen <code>mise run build</code>, <code>mise run test</code>, <code>mise run serve</code>. I Android-repoet ved siden af ligger de samme udsagnsord oven på Gradle i stedet, og et af dem pakker et task-navn på fire ord, jeg stadig ikke kan skrive udenad. Lige meget. <code>mise tasks</code> viser, hvad et repository kan, med beskrivelser, og svaret ligger i repoet frem for på en wiki-side, der holdt op med at være sand i 2023.</p>
<p>To små konventioner har gjort sig fortjent. Et bart udsagnsord er det almindelige tilfælde, og <code>verb:qualifier</code> indsnævrer det: <code>test</code> og <code>test:signer</code>, <code>stack:up</code> og <code>stack:down</code> og <code>stack:clean</code>. Der er også altid en nødudgang, som pakker det underliggende build-værktøj direkte, så alt, den kuraterede liste ikke dækker, stadig er én kommando væk:</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>Den nødudgang får betydning i anden halvdel af historien.</p>
<h2 id="halvdel-to-hemmelighederne">Halvdel to: hemmelighederne<a class="gn-heading-anchor" href="#halvdel-to-hemmelighederne" aria-label="Link til dette afsnit"><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>Det her er den del, jeg faktisk ville til livs.</p>
<p><code>.env.example</code>-mønstret er så udbredt, at det læses som best practice. Det er en konvention, der gav op. Se på, hvad den beder et team om.</p>
<p>En ny udvikler kopierer en fil med tomme nøgler og skal så skaffe rigtige værdier fra et sted, repoet ikke kan fortælle dem om. I praksis betyder det en person, hvilket betyder en chatbesked, og nu findes en hemmelighed i endnu et system med andre opbevaringsregler og anden adgangsstyring end det, man med vilje havde valgt. Når en værdi roteres, er enhver computer med den gamle stille og roligt forkert, indtil nogen opdager det, og fejlen dukker som regel op som en forvirrende 401 frem for en besked der siger &ldquo;dit login er fire måneder gammelt&rdquo;. Når en ny hemmelighed tilføjes, er alle andres <code>.env</code> ufuldstændig, og ingen får det at vide. Og filen selv ligger i dit arbejdskatalog i klartekst, ét uheldigt <code>git add -A</code> fra at blive committet, hvilket er grunden til, at ethvert repository med dette mønster også har en <code>.gitignore</code>-linje, nogen kunne slette.</p>
<p>Alternativet er ikke kompliceret. Læg værdierne i et hemmelighedslager, du allerede stoler på, læg <em>referencer</em> til dem i repoet, og lad den enkelte udviklers egen identitet afgøre, om de kan slås op.</p>
<p>Det er fnox. Her er stage-konfigurationen for en service, committet, uden værdier nogen steder:</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 class="nx">prefix</span> <span class="p">=</span> <span class="s2">&#34;/apps/checkout/stage/&#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><code>type</code> er den eneste linje, der er specifik for dig, og jeg har ladet den stå tom med vilje, fordi hvilket lager man bruger, er den mindst interessante beslutning i hele opsætningen. fnox taler med <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> og de store cloud-udbyderes egne hemmelighedslagre. Vi tog det, vores udbyder allerede gav os, af den kedelige grund at vi betalte for det, og det allerede havde en adgangsstyring, vi stolede på. Alt andet i filen er det samme, uanset hvad man vælger.</p>
<p>Hvad den fil køber:</p>
<p>Repoet dokumenterer sine egne hemmeligheder. Hvert navn, en service har brug for, står ét sted, i git, med reviewhistorik. Ny hemmelighed? Den står i filen, den kommer med <code>git pull</code>, og næste kørsel finder den enten eller fejler højlydt på grund af det <code>if_missing = &quot;error&quot;</code>. Ingen skal have noget at vide.</p>
<p>Rotation holder op med at være en begivenhed. Skift værdien i lageret; enhver maskine samler den op ved næste kørsel. Der er ingen flåde af forældede kopier.</p>
<p>Adgang bliver igen et autorisationsspørgsmål. Om du kan køre mod et givent miljø, afgøres af din identitet i hemmelighedslageret, ikke af om nogen huskede at sende dig en fil. At fjerne nogens adgang bliver en rettighedsændring i stedet for et håb om, at de sletter en fil.</p>
<p>Og der er en tørkørsel. <code>fnox check</code> fortæller dig, om hver erklæret hemmelighed kan slås op af dig, uden at udskrive nogen af dem. Det er det hurtigst mulige svar på &ldquo;er det mig eller er det i stykker&rdquo;, hvilket er det spørgsmål, der æder onboarding-dage.</p>
<p>Så én bevidst tilbageholdenhed. Den <code>env = &quot;exec&quot;</code>-linje betyder, at hemmeligheder aldrig injiceres i en interaktiv shell, kun i miljøet for en underproces, fnox starter. fnox vil med glæde gøre det modsatte; der findes en tilstand, som lægger alt ind i din shell i det øjeblik, du træder ind i kataloget. Vi bruger den ikke. Jeg vil ikke have, at enhver proces startet fra det katalog arver produktionsnære credentials, og det gælder også editorer, og det gælder de kodeagenter, jeg har kørende derinde det meste af dagen. At begrænse hemmeligheder til <code>fnox exec -- &lt;kommando&gt;</code> er hele forskellen mellem &ldquo;buildet kan se signeringsnøglen&rdquo; og &ldquo;alt på min maskine kan se signeringsnøglen&rdquo;.</p>
<p>Netop den del havde jeg undervurderet. Med intet på disken og intet i shellen findes en hemmelighed kun inde i en proces, som noget bevidst har startet, hvilket gør &ldquo;hvad kan agenten nå&rdquo; til en helt almindelig rettighedsbeslutning. Nægt den <code>fnox</code> og den håndfuld tasks, der kalder det, tillad den alt andet, og den kan læse hele repoet, bygge, teste og formatere, uden at en credential nogensinde havner i dens kontekst.</p>
<p>Så hullet, som jeg hellere selv vil sætte navn på, end at få det gjort for mig. fnox læser lageret som <em>mig</em>, med credentials der ligger på min maskine alligevel. En agent med en shell har slet ikke brug for fnox; den kan spørge lageret med udbyderens egen CLI. At nægte <code>fnox</code> alene lukker hoveddøren i en bygning uden vægge. Det, der gør det egentlige arbejde, er, at den samme tilladelsesliste heller ikke indeholder den CLI, og at det, der er inden for rækkevidde fra en udviklermaskine, er stage frem for produktion. En agent, der kan redigere <code>.mise.toml</code>, kan også skrive sig selv en task, der kalder hvad som helst. Nægtelsen er et hegn, ikke et bevis.</p>
<p>Men hold det op mod det, den erstatter: en <code>.env</code>-fil i arbejdskataloget i klartekst, læsbar for alt, der kan kalde <code>cat</code>, hvilket er enhver agent, nogen nogensinde har kørt. Injektion udelukkende ved kørsel plus en nægtelsesregel er, så vidt jeg kan se, omtrent så godt, som det bliver lige nu.</p>
<h2 id="hvor-de-to-halvdele-mødes">Hvor de to halvdele mødes<a class="gn-heading-anchor" href="#hvor-de-to-halvdele-m%c3%b8des" aria-label="Link til dette afsnit"><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>Hver for sig er det to udmærkede værktøjer. Sammen fjerner de en hel kategori af manuelle skridt.</p>
<p>Fordi mise-tasks bare er kommandoer, kan en task være <code>fnox exec -- noget</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>Nu er <code>mise run gradle :app:assembleRelease</code> et signeret release-build, og den, der kører det, skriver aldrig ordet fnox, lærer aldrig hvilke af buildets input der er hemmelige, og har aldrig en kopi af nogen af dem. Ingen skal huske, hvilke kommandoer der kræver hemmeligheder. Det gør tasken.</p>
<p>Der ligger også en til dem, der lever i en 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>En IDE ved intet om noget af det her, så et build startet indefra den kan ikke signere. Start IDE&rsquo;en <em>gennem</em> fnox, og dens Gradle arver miljøet. Det er én linje, og den er forskellen på et fungerende release-build og en signeringsfejl, ingen kan forklare, så den bliver i filen. Jeg griber ikke selv ud efter den ret tit: det meste af min dag er en terminal, en prompt og en agent, der skriver. Det er en præference, ikke en anbefaling, og tasken ligger der til den, der vil det modsatte.</p>
<p>Så er der opdelingen efter miljø. Én fil hver, de samme variabelnavne i dem alle, forskellige værdier bag navnene:</p>
<pre tabindex="0"><code>fnox.toml         # local
fnox.stage.toml   # stage
fnox.prod.toml    # production
</code></pre><p>En task peger på den fil, den har brug for. Alt, der hører til et miljø, ligger i det miljøs fil, så ingen kan komme til at parre stage-credentials med lokale indstillinger.</p>
<p>Den lokale er den fil, jeg ville pege på, hvis du kun læste én. Den erklærer næsten ingen hemmeligheder, fordi alt, hvad en lokal kørsel har brug for, allerede har en harmløs standardværdi committet i repoet: en lokal database, en mock foran alt eksternt, åbenlyst falske nøgler til de betalte tjenester. En frisk klon booter og svarer for en, der slet ikke har adgang til hemmelighedslageret, og alt, der ville nå en rigtig udbyder, fejler autentificering frem for at gøre noget dyrt. Adgang køber dig <em>stage</em>. Det er ikke prisen for at komme ind.</p>
<h2 id="at-committe-referencer-betyder-at-scanne-efter-værdier">At committe referencer betyder at scanne efter værdier<a class="gn-heading-anchor" href="#at-committe-referencer-betyder-at-scanne-efter-v%c3%a6rdier" aria-label="Link til dette afsnit"><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>At committe referencer til hemmeligheder virker kun, hvis du er sikker på, at intet ægte nogensinde smutter med. Så den samme opsætning bærer en scanner rundt, og mise installerer den, uden at nogen bliver bedt om det:</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> er det første, nogen kører, og den første <code>mise run</code> derefter installerer værktøjskæden, hvilket også er det, der udløser postinstall-hooket og peger git på et katalog med en pre-commit-scanning. Den kommer som en sidegevinst ved det skridt, de alligevel skulle tage, frem for som en separat instruktion, folk springer over.</p>
<p>Hooket scanner kun staged indhold, så det er hurtigt, og hvis mise ikke er på PATH, springer det over med en høj advarsel i stedet for at fejle. En grafisk git-klients miljø er ofte minimalt, og at blokere nogens commit på grund af et PATH-problem er en god måde at lære dem at bruge <code>--no-verify</code> for altid. CI scanner de samme commits igen efter push.</p>
<h2 id="og-deployments">Og deployments<a class="gn-heading-anchor" href="#og-deployments" aria-label="Link til dette afsnit"><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>Folk går som regel ud fra, at netop denne del virker anderledes, end den gør.</p>
<p>Vores deployede miljøer kører ikke fnox. Platformen, de kører på, læser selv de samme værdier fra det samme lager og giver dem til processen som ganske almindelige miljøvariable. Ingen applikationskode ved noget om det. Intet i runtimen importerer et bibliotek, og intet build skal have at vide, hvilket miljø det er til.</p>
<p>Det er med vilje. Lageret er sandheden. fnox er, hvordan et <em>menneske</em> læser den sandhed, med sine egne credentials, på sin egen maskine, og deploymentet læser samme sandhed med maskinens identitet. De to kan ikke komme fra hinanden, og applikationen ser på begge sider bare miljøvariable, som den altid har gjort.</p>
<h2 id="hvad-det-ikke-løser">Hvad det ikke løser<a class="gn-heading-anchor" href="#hvad-det-ikke-l%c3%b8ser" aria-label="Link til dette afsnit"><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>Den ærlige version, så.</p>
<p>Du skal stadig installere mise og lægge <code>mise activate</code> i din shell-profil. Det er et rigtigt opsætningsskridt, og det vil jeg ikke lade som om ikke findes. Det er ét skridt, det er det samme skridt i alle otteogtres repositories, og det bliver aldrig forældet, hvilket er tre ting, det afsnit det erstattede ikke kunne sige.</p>
<p>Docker er stadig dit eget. mise installerer værktøjer, ikke dæmoner. Vores lokale stacks kræver containere, og førstegangsoplevelsen for enhver uden Docker sat op er stadig et manuelt skridt og en download målt i gigabyte. Det samme gælder Xcode, som fortsat er sit eget vejrsystem.</p>
<p>At få værktøjskæden rigtigt fjerner den dumme del af en koldstart. Det gør intet ved den svære del, som er at forstå, hvad servicen egentlig laver. Det er stadig at læse kode, og det tager stadig dage.</p>
<p>Du lægger to afhængigheder oven i enhver udviklers hverdag. mise er modent og bredt anvendt. fnox er ungt. Vi har lagt rigtige arbejdsgange over i det, og går det i stå, står vi med en migrering. Jeg synes, det er et rimeligt væddemål: filen er deklarativ, værdierne ligger i et lager, vi ville beholde alligevel, og faldbacken er et shell-script, der læser de samme parametre. Men det <em>er</em> et væddemål, og at lade som om noget andet ville være uærligt.</p>
<p>Én linje i den konfiguration fortjener et grundigt review. fnox kan få besked på at køre en kommando, der logger dig ind i lageret, når din session er udløbet, hvilket er oprigtigt praktisk og samtidig betyder, at en committet konfigurationsfil nu bestemmer noget, der eksekverer på enhver reviewers computer. Læs den linje, som du ville læse et CI-workflow.</p>
<p>Der er også en fejltilstand med en vildledende besked. Peg udbyderen på den forkerte konto, og det, der kommer tilbage, er &ldquo;not found&rdquo;, hvilket læses som en manglende hemmelighed frem for en forkert identitet, og sender folk på jagt det helt forkerte sted. Begge vores filer bærer en kommentar, der siger præcis hvilken konto credentials skal slå op i.</p>
<h2 id="hvor-vi-faktisk-er">Hvor vi faktisk er<a class="gn-heading-anchor" href="#hvor-vi-faktisk-er" aria-label="Link til dette afsnit"><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>To repositories helt på fnox. En håndfuld mere på mise. Tres og lidt til tilbage, hvoraf de fleste aldrig får brug for hemmelighedshalvdelen.</p>
<p>Jeg har ikke taget tid på det med et stopur, og jeg vil ikke opfinde et tal, jeg ikke har målt. Formen på svaret er alligevel klar nok. På en maskine, der har været igennem det her én gang før, med mise installeret og værktøjskæden i cachen og Docker kørende, er en klon til en svarende API under et minut. Rigeligt. Det, der er tilbage af de gamle to til seks timer, er alt sammen første-gang-på-denne-computer: at hente en JDK, trække images, installere mise selv. Den betaler man én gang frem for én gang per repository, og netop den anden del er hele forskellen fra, hvor vi startede.</p>
<p>I de repositories, hvor det her er landet, gik README&rsquo;ens opsætningsafsnit fra en liste over ting at installere og et afsnit om at spørge en kollega ned til det her:</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>To linjer, og den anden er den, du ville have alligevel. Den første er mise, der spørger, om du nu også stoler på en konfigurationsfil, som er ved at installere software og sætte miljøvariable, hvilket, i betragtning af at den fil nu kan køre kommandoer på din maskine, er en dialog jeg er glad for findes.</p>
<p>Mønstret generaliserer et godt stykke ud over vores egen teknologiske virkelighed, og derfor synes jeg, det er værd at skrive ned frem for at arkivere som en intern pligtopgave.</p>
<p>Slet så opsætningsguiden, for nu lyver den om, hvor meget arbejde der er.</p>
]]></content:encoded>
      <category>Teknik</category>
      <category>Infrastruktur</category>
    </item>
    <item>
      <title>Vejen til stjernerne</title>
      <link>https://build.golfnext.com/da/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/da/posts/road-to-glory/</guid>
      <description>På et tidligere job omstrukturerede jeg 250.000 linjer kode, og en opgave, der havde taget to dage, begyndte at tage tre minutter. Ingen blev hundrede gange bedre til at programmere — omstruktureringen slettede bare alt det, der ikke var arbejdet. Og så begyndte frontendudviklere at tage backendopgaver. Det her er planen om at gøre det igen her, og den del af den, der bliver værre, før den bliver bedre.</description>
      <content:encoded><![CDATA[<p>Spørg en lille virksomhed, hvor arbejdet reelt venter, og svaret er næsten aldrig kapacitet. Det er routing.</p>
<p>En ændring bliver beskrevet på én sætning, alle er enige om, at den bør ske, og så ligger den stille — fordi det repository, den bor i, har præcis én person, der trygt kan åbne det. Ikke fordi vedkommende vogter over det, men fordi han er den eneste, der ved, at projektet vil have en JDK tre majorversioner bag den, du sidst brugte, og at det, der ligner indgangen, holdt op med at være indgangen i 2021.</p>
<p>Vi har over halvfjerds repositories hos GolfNext, bygget over tretten år i en halv snes sprog, og for en pæn del af dem er det antal mennesker ét. Det er emnet for det her indlæg; alt andet ligger nedstrøms for det.</p>
<p>&ldquo;Vejen til stjernerne&rdquo; er et ambitiøst navn til en plan, hvis første tre skridt er kedelige. Titlen er ikke ironisk ment: ambitionen er, at det her skal blive den platform, golfbranchen kører på, i hele verden — og kedeligt er det, en motor til det er lavet af.</p>
<h2 id="tallet-der-faktisk-gør-ondt">Tallet, der faktisk gør ondt<a class="gn-heading-anchor" href="#tallet-der-faktisk-g%c3%b8r-ondt" aria-label="Link til dette afsnit"><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="/da/author/jb/">Vores tech lead</a> har <a href="/da/posts/four-products-68-repos/">skrevet her</a> om tilstanden i vores kodelandskab, og jeg brugte <a href="/da/posts/in-the-jungle/">mine første to uger</a> på at finde ud af, hvilke repositories der stadig kunne starte på en frisk maskine. Ingen af delene var en klage. Over halvfjerds repositories er, hvad tretten års levering af software, der virker, til rigtige golfklubber ser ud som, og oprydningen er kun mulig, fordi produktet virker. Men der er en fejltilstand i det, de ikke satte navn på direkte, og det er den, der afgør, hvor hurtigt den her virksomhed kan vokse.</p>
<p>Når en kodebase har én vedligeholder, er tre ting sande på én gang, og ingen af dem er den persons skyld.</p>
<p><strong>Arbejdet står i kø bag ham.</strong> Alt, der rører det system, venter på den ene kalender, det kan vente på. Fint ved tre ting i kvartalet. Ødelæggende ved tre om ugen.</p>
<p><strong>Viden forlader aldrig hovedet på ham.</strong> Ikke fordi han ikke vil dele den, men fordi intet får den ud. Dokumentation gør det ikke: folk skriver om de dele, de selv fandt interessante, og det er per definition ikke de dele, en nyankommen sætter sig fast i.</p>
<p><strong>Ingen kan efterse hans arbejde.</strong> Et review foretaget af en, der ikke kan køre koden, er et review af diffens stavning.</p>
<p>Læg det sammen, og du har et system, der teknisk set ejes af virksomheden og i praksis ejes af en person. Vi har flere. Det har enhver virksomhed i den alder.</p>
<p>Der er en afledt omkostning, jeg først forstod ved at leve den: når en nyankommen støder på noget, der er i stykker, kan vedkommende ikke afgøre, om det er repositoryet, der er i stykker, eller ham selv. Jeg mistede dage på en lokal database, der viste sig at være umulig at bygge fra bunden — fire migreringer, i en kæde på 251, som ikke kunne køre mod en frisk import — og i de fleste af de dage gik jeg ud fra, at jeg var problemet. Det er den naturlige antagelse i sine første fjorten dage, og ingen kunne have rettet mig, for alle, der allerede var sat op, havde en database, der lå før bruddet. Til sidst holdt jeg op med at gå ud fra, at det var mig, og rettede det migrering for migrering.</p>
<p>Den told rammer kun nye mennesker, og det er præcis derfor, den er usynlig for dem, der kunne fjerne den. Det er også et af de renere argumenter for denne plan: når hvert repository opfører sig ens, har en nyankommen en kontrolgruppe. Hvis dette ene afviger fra de fem foregående, er det repositoryet, der er galt på den.</p>
<p>Og det skalerer den forkerte vej. Vi er i atten lande med mere end fire hundrede venues, og planen indebærer betydeligt mere af begge dele, så en bus factor på ét bliver ikke stående, når man vokser — den samme person bliver ganske enkelt flaskehals for flere ting.</p>
<h2 id="hvad-forskelligt-koster-post-for-post">Hvad &ldquo;forskelligt&rdquo; koster, post for post<a class="gn-heading-anchor" href="#hvad-forskelligt-koster-post-for-post" aria-label="Link til dette afsnit"><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>Det er nemt at sige, at en blandet stack er dyr, og svært at sige, hvor pengene går hen. Her er, hvor de går hen, i den rækkefølge jeg faktisk har mærket det.</p>
<p><strong>Skift.</strong> Bevæg dig mellem tre repositories på en formiddag, og du betaler en lille told ved hvert hop: hvilket byggeværktøj, hvilken version, hvilken kommando, hvilke idiomer. Hver for sig ubetydeligt. Det er frekvensen, der tager dig, og frekvensen er en funktion af, hvor mange forskellige faconer du ejer.</p>
<p><strong>Integrationer.</strong> Det er den, der betyder mest for vores forretning og bliver diskuteret mindst. Vores position i markedet er, at en klub beholder sit eget hardware og sit eget managementsystem, og at vi arbejder sammen med det, de allerede kører. Det betyder, at hver ny partner er endnu en integration, og prisen på en integration er groft sagt selve arbejdet ganget med, hvor fremmed den kodebase, du lægger den ind i, tilfældigvis er. Skriv den femte ind i samme facon som de første fire, og den koster en brøkdel. Skriv den ind i en kodebase med sine egne konventioner, og du betaler arkæologien om igen.</p>
<p><strong>Onboarding.</strong> En ny udvikler skal lære virksomheden én gang og derefter hvert system for sig. Anden del er der, hvor ugerne går. Hvis alle systemer har samme facon, er det næsten gratis at lære system nummer to, og det er forskellen på, om en ny person er brugbar efter fjorten dage eller efter et kvartal.</p>
<p><strong>Review.</strong> Standardisering er det, der gør et rigtigt review muligt for mere end én person. Hvis jeg kan køre din kode, kan jeg være uenig i den.</p>
<p><strong>Agenter.</strong> Vores tech lead har skrevet, at de repositories, hvor han stoler mest på en kodeagent, uden undtagelse er dem, nogen allerede har ryddet op i. Det passer præcist med min egen erfaring. En agent, der arbejder i en klar, konsistent struktur, producerer noget, du kan bruge. En agent i en ukendt struktur gætter flydende, og du bruger eftermiddagen på at finde ud af, hvilke dele der var gæt. Konsistens er ikke længere bare rart for mennesker. Det er det, der afgør, om halvdelen af vores værktøjskasse virker.</p>
<h2 id="ens-facon-slår-ens-sprog">Ens facon slår ens sprog<a class="gn-heading-anchor" href="#ens-facon-sl%c3%a5r-ens-sprog" aria-label="Link til dette afsnit"><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>Her er den del, jeg vil diskutere med hvem som helst, for det er den, folk vender om.</p>
<p>&ldquo;Lean tech stack&rdquo; bliver hørt som &ldquo;færre sprog&rdquo;, og færre sprog er kun cirka en tredjedel af gevinsten. De tre ting, der afgør, om du kan gå ind i et ukendt repository og være til nytte, er nogenlunde i denne rækkefølge:</p>
<ol>
<li>Er filstrukturen den samme? Ligger en given slags ting på en forudsigelig sti, under et forudsigeligt navn, i hvert repository?</li>
<li>Er udsagnsordene de samme? Kan du bygge, teste og køre det med de kommandoer, du brugte i det forrige repository?</li>
<li>Er det samme sprog og framework?</li>
</ol>
<p>Nummer tre er den, der får overskriften, og den mindst vigtige af de tre. Jeg har bevæget mig mellem to Java-services med vidt forskellige pakkestrukturer, og det var sværere end at bevæge sig mellem en Java-service og en TypeScript-service, der var lagt op ens.</p>
<p>Nummer et og to er også langt billigere at rette. Udsagnsordene har vi stort set løst allerede: mise lægger et repositorys værktøjskæde og dets kommandoer i en fil i repoet, så <code>build</code>, <code>test</code> og <code>run</code> betyder det samme alle de steder, det er rullet ud, uanset hvad der ligger nedenunder. Det var vores tech leads arbejde, og det er det, der har givet mest afkast per indsats af alt, nogen har lavet her.</p>
<p>Filstrukturen er den anden, og det er den, jeg ville lægge rigtig arbejde i, for det er den, man bruger, før man har læst en eneste linje. Man finder ikke kode ved at læse den. Man finder den ved at gætte en sti. Når hvert repository er enige om, hvor handlerne bor, hvor databasekoden bor, hvor testene bor, og hvad filerne hedder, er det gæt rigtigt første gang, og en søgning, der virkede i ét repository, bliver ved med at virke i det næste. Når de er uenige, er hvert repository en andens arkivsystem, og man åbner fire forkerte filer, før man rammer den rigtige — hvilket er det meste af, hvad den første time i en ukendt kodebase faktisk består af.</p>
<p>Den er også den billigste af de tre at rette, for den kræver slet ingen omskrivning. Det er en aftale plus tålmodigheden til at følge den på den fil, man alligevel har åben. Vores tech lead gjorde præcis det, da han tog React-proof of concept&rsquo;et ind i det rigtige repository: en omgang på, hvordan mapper og filer navngives, før der blev bygget noget oven på det. Og det er den del, en agent mærker mest, af samme grund som du gør — den finder kode ved at forudsige, hvor koden burde være.</p>
<p>Det er nyttigt at vide, for det betyder, at det meste af gevinsten ved en lean stack er tilgængelig, før man har konsolideret et eneste sprog. Det betyder også, at sprogkonsolidering skal retfærdiggøres på egne meritter frem for at låne æren for de to andre.</p>
<h2 id="to-en-halv-dag--tre-minutter">To en halv dag → tre minutter<a class="gn-heading-anchor" href="#to-en-halv-dag--tre-minutter" aria-label="Link til dette afsnit"><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>Indtil videre er det her et argument, og argumenter er billige. Så her er, hvorfor jeg tror på det.</p>
<p>På et tidligere job brugte jeg en lang periode på at omstrukturere omkring 250.000 linjer kode. Ikke en rewrite. Samme sprog, stort set samme logik, omorganiseret: hvor tingene bor, hvad der må afhænge af hvad, og hvilken facon en ændring skal have for at passe ind.</p>
<p>Før det tog en simpel opgave — den helt almindelige, tilbagevendende slags, der udgør det meste af en uge — to til tre dage. Bagefter blev jeg nysgerrig nok til at tage tid på en af dem, og den landede på tre minutter.</p>
<p>Jeg skal være præcis om det tal, for det er den slags tal, der bliver gentaget, indtil det bliver til en løgn. Tre minutter var én opgave, målt én gang. Andre tog fem minutter eller ti. Det, der ændrede sig, var ikke, at hver opgave blev en tre-minutters opgave. Det var, at enheden ændrede sig: arbejde talt i dage blev nu talt i minutter, og intet af det gik tilbage.</p>
<p>Selv den forsigtige ende af det er et forhold, ingen bør tro om sig selv, så her er, hvad det faktisk betyder. Ingen blev hundrede gange bedre til at programmere. Selve arbejdet havde altid været lille — et par minutters tastearbejde, begravet under alt det, der ikke var arbejdet: at finde ud af hvor ændringen hørte hjemme, at læse nok af den omkringliggende kode til at være sikker, at regne ud hvad der ellers rørte ved den, og så den del, der aldrig havner på en timeseddel, nemlig at være bange for at ødelægge noget, man ikke forstår, og bevæge sig langsomt med vilje på grund af det.</p>
<p>Omstruktureringen gjorde ikke arbejdet hurtigere. Den slettede alt det, der ikke var arbejdet.</p>
<p>Det fortæller også, hvilke opgaver der falder sammen, og hvilke der ikke gør. Reelt nyt arbejde, hvor det svære er at beslutte, hvad tingen skal gøre, tog omtrent lige så lang tid bagefter som før; struktur hjælper dig ikke med at tænke. Det, der falder sammen, er den tilbagevendende facon — den femte integration, endnu et felt på en formular, det endpoint der ligner de elleve ved siden af. I et landskab som vores er det det meste af køen.</p>
<h2 id="resultatet-jeg-faktisk-ville-pege-på">Resultatet jeg faktisk ville pege på<a class="gn-heading-anchor" href="#resultatet-jeg-faktisk-ville-pege-p%c3%a5" aria-label="Link til dette afsnit"><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>Og så den del, jeg ikke havde forudset. Frontendudviklere begyndte at tage backendopgaver.</p>
<p>Ingen bad dem om det. Når hver service er lagt op på samme måde, viser barrieren sig at have været strukturel snarere end et spørgsmål om viden. Det er den mest brugbare dokumentation i dette indlæg, fordi den ikke er selvrapporteret: et produktivitetstal er noget, man siger om sig selv, og at krydse en grænse, man har undgået i årevis, er noget, man gør. Det flyttede det tal, indlægget åbnede med, uden at ansætte nogen.</p>
<p>Én ting mere, for denne blog skriver konstant om kodeagenter, og det ville være nemt at læse alt det som en AI-historie. Der var ingen AI i den. Slet ingen. Hvert eneste minut af gevinsten kom fra, hvor koden lå, og hvad der måtte afhænge af hvad — hvilket betyder, at gevinsten ikke er betinget af, at agenter bliver ved med at blive bedre, eller at vi bruger dem godt. Struktur betalte sig, før noget af det her fandtes. Det, agenter gør, er at gange den op, mest af alt i de repositories, der allerede har den. Hvis de gik i stå i morgen, ville argumentet i dette indlæg være uændret.</p>
<p>Det vil jeg gerne gentage her. Anden virksomhed, andet kodelandskab, samme mekanisme. Den omstrukturering er også dér, indlæggets titel kommer fra, og det er grunden til, at jeg kan skrive en plan så sikkert om arbejde, der endnu ikke er udført: jeg forudsiger ikke en effekt, jeg forventer en, jeg allerede har stået midt i.</p>
<h2 id="hvad-motoren-er-til-for">Hvad motoren er til for<a class="gn-heading-anchor" href="#hvad-motoren-er-til-for" aria-label="Link til dette afsnit"><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>Et indlæg om at gøre udviklere mere produktive kan læses forkert, så lad mig lukke den læsning selv. Det her handler ikke om at få brug for færre udviklere. Ingen her gør det, for at det samme arbejde kan udføres af færre mennesker, og hvis det var planen, ville jeg ikke skrive et blogindlæg om den. Udviklerne her er dygtige. De har bygget et produkt, der virker i atten lande, under vilkår jeg aldrig selv har skullet leve med, og at miste nogen af dem ville sætte det her tilbage frem for frem.</p>
<p>Grunden er vækst, og jeg vil hellere sige størrelsen på den end pakke den ind. Vi prøver ikke at blive lidt større. Målet er, at det her bliver den platform, golfbranchen kører på — hver klub, hver range, hvert land der har begge dele — og grunden til, at det kan siges højt frem for at være pinligt, er, at efterspørgselssiden allerede melder sig. De klubber, der ser produktet, er efter min erfaring indtil videre oprigtigt imponerede. Den reaktion er aktivet. Intet i dette indlæg skaber den, og intet i dette indlæg kan erstatte den.</p>
<p>Det, vi ikke har, er motoren til at betjene den i den størrelse. Produktet er godt; maskineriet, der bygger det, integrerer det og supporterer det, er tretten års akkumulerede beslutninger, og det er den del, der knækker først, hvis efterspørgslen ankommer hurtigere, end teamet kan optage den. Vi bygger ikke produktet — det findes, og folk, der ikke har haft noget med at bygge det at gøre, er imponerede over det. Vi bygger det, der ligger under, og som afgør, hvor mange klubber, hvor mange lande og hvor mange integrationer et lille team kan bære på én gang. Imponerede kunder er det, der har bragt os til mere end fire hundrede venues i atten lande. Hvad der afgør, om tallet derefter får et nul mere, er, om den ellevte integration koster det samme som den første.</p>
<p>Derfor er den del af det her, jeg mest går op i at få rigtigt, heller ikke sproget. Det er, at de mennesker, der skal leve i resultatet, gerne vil.</p>
<h2 id="react-foran-go-bagved">React foran, Go bagved<a class="gn-heading-anchor" href="#react-foran-go-bagved" aria-label="Link til dette afsnit"><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>Det, man leder efter, når man endelig vælger et sprog, er altså ikke det mest udtryksfulde. Det er det, der giver en de to andre gratis.</p>
<p>Målet er to sprog.</p>
<p>Alle brugerflader er React. Web er React og TypeScript, mobil er React Native, og det gælder også fladerne, der sidder på hardwaren — den kioskskærm, en spiller står foran klokken halv seks om morgenen, er en skærm som alle andre. Det var allerede halvt sandt, før nogen besluttede det, fordi vores mobilapplikationer er React Native, og det blev sandt i praksis, da en af vores frontends gik fra Vue til React og kom i produktion. Resten følger efter.</p>
<p>Alt bag de flader er Go. Ikke &ldquo;de nye services&rdquo; — alt. Webbackendene, det offentlige API, integrationerne, og desktop-kasseapplikationen, som bliver en Go-kerne med en React-flade i stedet for den JavaFX-applikation, den er i dag. Om en ting kører i et datacenter eller på en kasse i en proshop holder op med at være en oplysning om, hvilket sprog den er skrevet i.</p>
<p>Jeg vil ikke argumentere for sprogene her. Go over for alternativerne er et indlæg for sig, og den ærlige opsummering er, at det konkrete valg betyder betydeligt mindre end, at der er ét — hvilket er det, hele dette stykke har argumenteret for. Én ting skal dog siges rent ud inden næste afsnit: Go er ikke det, vi har mest af. Java, PHP, Node og Kotlin har alle mere kørende kode bag sig i dag, og det er en reel omkostning snarere end en detalje.</p>
<p>Det, beslutningen køber, er to ting, og de er værd at holde adskilt, fordi de bliver rørt sammen i ethvert indlæg som det her. Det første er udvikleroplevelse, som er det meste af, hvad dette indlæg handler om, og som koger ned til tallet i toppen: hvor mange mennesker kan åbne et givet repository og være til nytte i det.</p>
<p>Det andet er performance, og der vil jeg hellere være præcis end sloganagtig, for &ldquo;simpel stack, altså hurtig&rdquo; er ikke et argument. Hvad det derimod betyder, er to bestemte ting. Den ene er fodaftryk: hvad en proces koster at starte og at holde kørende, hvilket er et rigtigt tal på kioskhardware og en rigtig linje på en cloud-regning. Den anden er, at performancearbejde er betinget af forståelse — man kan ikke rette det langsomme sted i et system, ingen kan rumme i hovedet, og de fleste systemer er langsomme et sted, deres vedligeholdere holdt op med at kigge for år siden.</p>
<p>Én delbeslutning er reelt ikke afgjort. En Go-kerne med en React-flade på desktoppen har brug for en skal til at holde den flade, og den har vi ikke valgt. Faconen er besluttet; mekanismen er ikke, og det haster ikke, for desktopapplikationen er langtfra forrest i køen.</p>
<h2 id="slaget-vi-ikke-slår">Slaget vi ikke slår<a class="gn-heading-anchor" href="#slaget-vi-ikke-sl%c3%a5r" aria-label="Link til dette afsnit"><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>På de fleste baner er der et hul, hvor den ambitiøse linje går lige over vandet. Den store rewrite er det slag: frys det gamle, byg det nye ved siden af, skift over. Hurtigst, hvis det lykkes — og kirkegården er fuld af virksomheder, der brugte to år på ikke at levere noget, mens deres konkurrenter leverede features.</p>
<p>Vi lægger op. Fire regler.</p>
<p><strong>Nyt arbejde bygges i målfaconen.</strong> Den eneste gratis regel, og den der udretter mest over et år: hver ny service er et repository, der aldrig skal konverteres.</p>
<p><strong>Skriv kun om, hvad du alligevel skulle åbne.</strong> Konvertering er billigst, når nogen alligevel betaler for at forstå systemet. Software, der virker, og som ingen har brug for at ændre, er ikke et problem; det er et aktiv med et grimt logo.</p>
<p><strong>Sømme før rewrites.</strong> To adminportaler, to backends, to logins. Ikke en omskrivning til én: en auth-bro først, så én frontend oven på begge backends, og først derefter konsolidering af backenden. Hvert skridt kan leveres for sig — og opgives for sig.</p>
<p><strong>Struktur og udsagnsord først, overalt; sprog sidst, selektivt.</strong> De billige to tredjedele er tilgængelige i hvert eneste repository i dag, inklusive dem der aldrig bliver konverteret.</p>
<p>Og så det, jeg hellere vil skrive ned end blive fanget i ikke at have tænkt over: det bliver mere broget, før det bliver mindre. Go er ikke et sprog, vi har meget af, så et par år frem indeholder kodelandskabet Java, PHP, Node, Kotlin <em>og</em> Go. Konvergensen ankommer sidst. Vil man diskutere den her plan, skal man diskutere dér — det er det svageste punkt i den, og det eneste forsvar er, at de fire regler findes for at gøre det mellemrum til at holde ud.</p>
<p>Én advarsel frem for en regel: agenter har gjort rewrites billige at producere og ikke en krone billigere at gennemgå. En rewrite, der ser færdig ud, er det mest smigrende, de laver, for dens huller er udeladelser, og en udeladelse dukker aldrig op i en demo.</p>
<h2 id="hvad-det-faktisk-koster">Hvad det faktisk koster<a class="gn-heading-anchor" href="#hvad-det-faktisk-koster" aria-label="Link til dette afsnit"><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>Planen har en regning, og jeg vil hellere gøre den op selv end lade en anden gøre det.</p>
<p><strong>Monokultur er en reel risiko, ikke en retorisk.</strong> To sprog overalt betyder to sæt blinde vinkler, to afhængighedsøkosystemer hvis dårlige uger nu er alles dårlige uger, og et team der gradvist mister refleksen til at spørge, om det sædvanlige værktøj nu også er det rigtige.</p>
<p>Den stærkeste udgave af den indvending er det rigtige værktøj til opgaven, og generelt er jeg enig i den. Men det er et argument om problemområder, og vores er ét problemområde. Tag etiketterne af vores systemer, og de laver alle sammen genkendeligt det samme arbejde: tag imod en transaktion, tal med en enhed eller en partners API, og læg en post et sted, man kan stole på bagefter. Der er ingen machine learning-arbejdsbyrde her, der trækker mod Python, og ingen signalbehandling, der trækker mod C. Variationen i vores kodelandskab svarer ikke til nogen som helst variation i de problemer, det løser, og det er tegnet på, at den er tilfældig snarere end tilsigtet. Ingen valgte en halv snes sprog som en afdækningsstrategi.</p>
<p>Så risikoen er virkelig og værd at sige højt, og forholdsreglen er, at &ldquo;standard&rdquo; skal betyde en stærk default med en dokumenteret måde at argumentere imod på, ikke en regel. Den dag et reelt anderledes problem dukker op — og i en virksomhed, der laver mere med AI for hvert kvartal, kan det ske — skal det argument kunne vindes, ellers er defaulten i stilhed blevet til en regel alligevel.</p>
<p><strong>Konsolidering kan være en måde at undgå produktarbejde på.</strong> Strukturelt arbejde er tilfredsstillende og letlæseligt, og det skaber ingen kundeværdi den dag, det lander. Disciplinen er, at hvert skridt skal retfærdiggøres af noget, det låser op. Sammenlægningen af portalerne er retfærdiggjort, fordi klubpersonale logger ind to gange. React-standardiseringen er retfærdiggjort, fordi den er forudsætningen for alt det, der kommer efter. Hvis et skridt ikke kan sige, hvad det låser op, er det ikke klar.</p>
<p><strong>Det erstatter arbejde, folk er stolte af.</strong> Det var det, ledelsen bragte op, da vi fremlagde planen, før omkostning og før kunderisiko, og de havde ret. Ingen har lyst til at høre, at det, de byggede godt under de vilkår de faktisk havde, nu er den gamle måde. Vi holdt en brainstorm med hele teamet og holdt bevidst vores eget forslag tilbage, indtil alle andre havde talt. Ingen var uenige. Delene om at slå portalerne sammen var folk oprigtigt begejstrede for; selve standardiseringen var folk okay med, hvilket er en rimelig ting at føle, når det er ens eget valg, der bliver erstattet, og det vil jeg hellere gengive korrekt end opgradere.</p>
<p><strong>Og det kommer til at tage længere tid, end dette indlæg får det til at lyde.</strong> Golfklubber har travlt om sommeren, så vi har travlt om sommeren. Platformen bliver stille hen over vinteren, og det er der, det strukturelle arbejde har plads. Det her er en plan målt i sæsoner.</p>
<h2 id="hvordan-vi-ved-om-det-virkede">Hvordan vi ved, om det virkede<a class="gn-heading-anchor" href="#hvordan-vi-ved-om-det-virkede" aria-label="Link til dette afsnit"><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>Ikke ud fra et diagram. Ud fra fire ting, man rent faktisk kan tjekke.</p>
<p>Hvor mange repositories har præcis én person, der trygt kan sende en ændring afsted til dem. I dag er det tal ubehageligt. Det er hovedmålet for hele denne plan, og det eneste jeg vil forsvare som handlende om virksomheden frem for om ingeniørsmag.</p>
<p>Hvor lang tid fra klon til kørende kode, på en maskine der har været igennem det én gang. Det var to til seks timer, da vores tech lead målte det i august, afhængigt af hvilket repository man valgte. I de repositories, mise og fnox har nået, er det én kommando.</p>
<p>Ambitionen bag det tal er mere kontant end tallet selv, så lad mig sige det, som vi faktisk siger det: at få en kodebase til at køre lokalt skal være så tæt på idiotsikkert, som vi kan gøre det. Ikke fordi nogen her er en idiot — tværtimod. Hvis det kræver kløgt at starte et repository, bliver den kløgt brugt på det forkerte, og den bliver brugt igen af hver eneste person, der kommer efter dig. Klon det, kør én kommando, få et fungerende system med en database, testdata og en konto at logge ind med. Alt, der står mellem de to punkter, er en defekt, uanset om det står i et tracker-system eller ej.</p>
<p>Hvad den anden integration koster i forhold til den første. Hvis vi laver det strukturelle arbejde, og den femte partnerintegration stadig koster det samme som den første, gjorde arbejdet ikke det, vi sagde, det ville.</p>
<p>Og hvad en ny udvikler kan nå ved udgangen af sin anden uge. Ikke &ldquo;har læst kodebasen&rdquo;. Har sendt noget i produktion hos en klub.</p>
<p>Den sidste er hele planen komprimeret til én sætning, og den er grunden til det hele. Alt i dette indlæg — React på skærmene, Go bagved dem, mappestrukturerne, auth-broen, den ene kommando der betyder det samme i hvert repository — findes, for at svaret på &ldquo;kan du tage den her?&rdquo; holder op med at afhænge af, hvilket repository vi taler om.</p>
<p>Og grunden til at gå op i et så lille spørgsmål er, at svaret på det sætter loftet for alt ovenover. En klub et sted beslutter, at vi er det bedste, de har set i år, og fra det øjeblik er det eneste spørgsmål, der betyder noget, om vi kan bygge det, de har brug for, mens de stadig er begejstrede for det. At gøre det fire hundrede gange er et produkt. At gøre det fire tusind gange er en motor.</p>
<p>Så her er den version, jeg faktisk tror på, skrevet ned et sted, hvor I kan holde mig fast på den.</p>
<p>Om to år tager en udvikler, der startede i sidste måned, en integration med et managementsystem i et land, vi endnu ikke sælger i. Han har aldrig åbnet det repository. Han kloner det, <code>mise run serve</code> henter værktøjskæden og starter tingen op, Go-handlerne ligger, hvor Go-handlere altid ligger, React-skærmene er lagt op som hver eneste anden React-skærm vi ejer, og testene kører igennem i første forsøg. Om torsdagen er det live hos en klub, der aldrig havde hørt om os i foråret. Ingen i huset synes, det er bemærkelsesværdigt.</p>
<p>Det er hele ambitionen. Ikke et heroisk kvartal — en torsdag.</p>
<p>Gang den torsdag med hver udvikler vi har og hvert land, der har golf i sig, og du får en virksomhed, der leverer lige så hurtigt, som den kan tænke, i en branche der aldrig har haft software, som bevægede sig i den fart. Produktet, folk bliver imponerede over, findes allerede; vi skulle ikke opfinde det, og jeg kan ikke tage æren for det. Det, vi bygger nu, er det eneste, der står mellem det produkt og hver eneste klub på kloden, som ville elske at have det — og vi vil hellere bygge det, før nogen andre regner ud, at det er dét, løbet går på.</p>
<p>Så: stjernerne er et stort ord, og det er det rigtige. En banerekord er ikke ét mirakuløst slag — det er atten helt almindelige huller spillet af en, der er holdt op med at tabe slag i roughen. Vi lægger ikke op til helteslaget over vandet. Vi fjerner hvert eneste sted, bolden kan blive væk, og så spiller vi vores livs runde.</p>
<p>Hvilket i praksis er lige så uglamourøst, som det lyder, og dér vil jeg hellere slutte end på metaforen. React på hver skærm og Go bag hver eneste af dem. Den samme mappestruktur i hvert repository, så man kan gætte en sti, før man åbner træet. Én kommando, der bygger hvad som helst af det. Hemmeligheder i et store i stedet for i en chatbesked. Over halvfjerds repositories, der konvergerer mod én facon — og en virksomhed, der derefter kan tage det hundrede ind, uden at nogen skal være modig.</p>
]]></content:encoded>
      <category>Arkitektur</category>
      <category>Teknik</category>
    </item>
    <item>
      <title>Inde i junglen</title>
      <link>https://build.golfnext.com/da/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/da/posts/in-the-jungle/</guid>
      <description>En del af min onboarding var en opgave med mit navn på: få hver enkelt kodebase til at køre lokalt. Enoghalvfjerds repositories, et årti af teknologivalg og to uger med at finde ud af, hvilke af dem der stadig kunne starte på en frisk maskine — inklusive den, hvis lokale database slet ikke kunne bygges, og hvad der skulle til for at få én kommando til at bygge den alligevel.</description>
      <content:encoded><![CDATA[<p>Jeg startede hos GolfNext i begyndelsen af august. Et sted i onboardingen, mellem konti og hardware, lå en opgave med mit navn på: få hver enkelt kodebase til at køre lokalt.</p>
<p>Ikke læse dem. Køre dem. Klon repositoryet, installér hvad det nu kræver, start tingen, og se den komme op på din egen maskine.</p>
<p>Det lyder som noget, man giver en ny person for at holde ham beskæftiget en dag. Det løb gennem det meste af mine første to uger, det gav mig det mest brugbare kort over denne virksomhed, jeg har, og det blev til en pæn mængde reparationsarbejde, ingen havde bedt om — for flere af de repositories kunne slet ikke starte.</p>
<p>Alene i min første uge arbejdede jeg i et Vue-administrationspanel, en Android-app der kører på en selvbetjeningskiosk, en Java-backend og en desktop-kasseapplikation, der udsendes som en Windows-eksekverbar fil.</p>
<p>Det er formen på virksomheden. GolfNext bygger den software, golfklubber kører på — booking og betaling for en runde, kassen i proshoppen, de ubemandede maskiner ude på træningsbanen — og efter mere end et årti med det er der enoghalvfjerds repositories i vores GitHub-organisation. Mest TypeScript, JavaScript og Java, med PHP bagefter, og så nogle få enkeltstående: Kotlin, HCL til infrastrukturen, shell.</p>
<p>Intet af det er usædvanligt for en virksomhed på den alder, og intet af det er en klage. Det findes, fordi mennesker har leveret et årti af fungerende produkt under virkelige begrænsninger, og den oprydning, jeg er ved at beskrive, er kun mulig, fordi produktet allerede virker. Det betyder til gengæld, at hvert repository er et lille stykke jungle. To ting var for alvor svære, og ingen af dem var selve programmeringen: at finde ud af, hvilket af enoghalvfjerds repositories den ting, jeg havde brug for, overhovedet lå i — og derefter at finde ud af, hvordan man startede det. Det sidste var opgaven.</p>
<h2 id="hvad-junglen-opkræver">Hvad junglen opkræver<a class="gn-heading-anchor" href="#hvad-junglen-opkr%c3%a6ver" aria-label="Link til dette afsnit"><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>Store forhindringer er interessante, og dem husker man. Junglen opkræver i stedet i små identiske beløb, igen og igen.</p>
<p>Man kloner noget, og buildet fejler, fordi din JDK er én major-version forkert. Det retter du, og så vil byggeværktøjet have en anden. Du går videre til et webprojekt, og versionsfilen siger én Node, mens lockfilen er bygget mod en anden. Et sted findes en miljøfil, der skal udfyldes, og værdierne bor hos en kollega. Hver af dem er femten eller tyve minutter, ingen af dem lærer dig noget, og du betaler dem igen på den næste maskine og igen for den næste, der starter.</p>
<p>Intet af det var ét enkelt repositorys skyld. Det var fraværet af et fælles svar: hver Java-applikation havde lagt sig fast på sin egen JVM-version, og hvert webprojekt på sin egen Node-version — i praksis afgjort af, hvad der tilfældigvis var aktuelt, sidst nogen satte det op, uden at nogen nogensinde gik tilbage og bragte dem i overensstemmelse. Afhængighederne nedenunder skred på samme måde — de samme biblioteker i forskellige versioner i forskellige repositories — så det, man lærte ved at få ét repository til at bygge, kunne ikke overføres til det næste.</p>
<p>Og at få nogen af dem til at køre lokalt var en afgift i sig selv — i en ny valuta hver gang. Det tydeligste eksempel: to af vores Java-desktopapplikationer er samme slags program, bygget til samme opgave — den ene bygger med Gradle, den anden med Maven. Ingen af de to repositories skriver en grund ned. På et tidspunkt holdt jeg op med at tage noter og stillede spørgsmålet højt, og det er i virkeligheden det spørgsmål, hele dette indlæg handler om: hvor er standardiseringen? Ikke som en klage. Som et reelt spørgsmål om, hvordan et landskab ender med at have to svar på et problem, der kun har ét.</p>
<h2 id="én-person-én-applikation">Én person, én applikation<a class="gn-heading-anchor" href="#%c3%a9n-person-%c3%a9n-applikation" aria-label="Link til dette afsnit"><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>Der er en strukturel grund til det, og jeg tror, den forklarer det meste af, hvad jeg fandt. I størstedelen af virksomhedens levetid er softwaren blevet bygget af fjernudviklere, der arbejdede på afstand fra andre lande og, så vidt jeg kan se, i høj grad hver for sig: én person, én applikation, fra start til slut. Er man den eneste, der nogensinde skal bygge en ting, giver det ingenting at erklære dens værktøjskæde i repositoryet. Man har den allerede — den ligger på ens maskine, og den er rigtig, fordi den er ens egen. Konventioner nærmer sig hinanden, når mennesker er nødt til at arbejde inde i hinandens repositories, og i årevis var der næsten ingen, der gjorde det.</p>
<p>Jeg vil tilføje en blødere teori, og jeg vil gerne være klar på, at det er en teori. Afstand er ikke kun geografisk. At arbejde på tværs af lande, tidszoner og andetsprog gør de billige samtaler dyre — dem, hvor to udviklere i forbifarten opdager, at de har løst det samme problem forskelligt, og bliver enige om én måde. Ingen behøver at være uenige, før et landskab skrider fra hinanden. Det er nok, at samtalen aldrig lige er belejlig. Intet af det er en kritik af nogen, der arbejdede her før mig; det er omtrent det, jeg ville forvente af den slags arrangement, og jeg havde formodentlig selv bygget den samme jungle i deres position.</p>
<p>Alt det har et navn, og det er branchens ældste vittighed: det virker på min maskine. Ingen her siger det med et skuldertræk, og ingen forsvarede det — men det er det, landskabet stille og roligt havde indrettet sig omkring. Hvis alle maskiner, der bygger et givet repository, allerede er sat op, så bor opsætningen ikke i repositoryet. Den bor i nogle få menneskers hoveder og i deres shell-historik. At få en ny person i gang betyder at læse den ud af dem igen, og den bliver aldrig komplet, for de dele, de gør automatisk, er præcis de dele, de glemmer at nævne.</p>
<h2 id="det-repository-der-ikke-kan-starte">Det repository, der ikke kan starte<a class="gn-heading-anchor" href="#det-repository-der-ikke-kan-starte" aria-label="Link til dette afsnit"><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>Det værre tilfælde er det repository, der slet ikke kan starte.</p>
<p>I en af vores backends var den lokale udviklingsprofil — den konfiguration, der peger applikationen mod en database på din egen maskine — blevet slettet måneder tidligere, og ingen havde bemærket det, fordi ingen, der allerede var sat op, nogensinde havde brug for den. Klon den forfra, og du får en datakildefejl, der intet fortæller dig om hvorfor. Under den lå noget værre: en databasemigrering skrevet for år tilbage, som ikke lader skemaet blive færdigt, før den har kaldt ud til en tredjepartstjeneste — en, denne kodebase ikke længere kan nå. Så en reelt ny lokal database kunne slet ikke bygges, på nogen profil.</p>
<p>Ingen af de to stod i et tracker-system. De er ikke fejl i almindelig forstand, og ingen gjorde noget forkert for at skabe dem. De er, hvad et repository glider ind i, når ingen frisk maskine nogensinde er den, der bliver testet — og man finder først ud af det den dag, nogen starter, eller nogens laptop bliver skiftet, hvor hele regningen så kommer på én gang.</p>
<h2 id="to-hundrede-og-enoghalvtreds-migreringer">To hundrede og enoghalvtreds migreringer<a class="gn-heading-anchor" href="#to-hundrede-og-enoghalvtreds-migreringer" aria-label="Link til dette afsnit"><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>Den sværeste at få startet var vores GolfMore-administrationspanel — det backoffice, klubpersonalet logger ind i, og som serverer det offentlige API ud af samme kodebase. PHP og Symfony, MySQL nedenunder, og et årti af skemahistorik liggende på disken: et 114 KB base-dump, der skal importeres først, og derefter 251 migreringsfiler, der skal køres oven på det.</p>
<p>Den dokumenterede vej til en lokal database var præcis det. Importér dumpet, kør migreringskæden, få et fungerende skema. Det virker ikke, og har ikke gjort det i årevis, fordi fire migreringer fra 2022 og 2023 ikke kan køre mod en database bygget på den måde:</p>
<ul>
<li>Én dropper den bogholderitabel, der er en rest fra det migreringsværktøj, projektet brugte før Doctrine, og som en frisk dump-import aldrig har haft.</li>
<li>Én kaster ubetinget en fejl i sin <code>postUp()</code>, fordi den kalder <code>addSql()</code> på en migrering, der allerede er frosset.</li>
<li>Én refererer til en tabel flere måneder før den migrering, der opretter den.</li>
<li>Én erklærer <code>AUTO_INCREMENT</code> på en kolonne uden nøgle, hvilket MySQL afviser, så den har aldrig kunnet køre overhovedet.</li>
</ul>
<p>Ingen har skylden her på nogen interessant måde. Den samlede virkning er problemet: den ene vej, en ny udvikler bliver bedt om at gå, er den ene vej, der er brudt, og alle, der allerede er sat op, har en database, der ligger før problemet, så ingen andre støder nogensinde ind i det.</p>
<h2 id="hvordan-en-kæde-rådner">Hvordan en kæde rådner<a class="gn-heading-anchor" href="#hvordan-en-k%c3%a6de-r%c3%a5dner" aria-label="Link til dette afsnit"><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>Hvordan det er endt sådan, kan jeg kun gætte på, og det vil jeg hellere sige end lade som andet. Mit bedste gæt er, at skemaet i produktion på et tidspunkt er blevet ændret i hånden. Den fjerde af de defekter er næsten bevis på det: MySQL har aldrig accepteret en auto-increment-kolonne, der ikke er en nøgle, så den migrering kan aldrig have kørt nogen steder — og alligevel findes den tabel, den beskriver, alle de steder, applikationen kører. Noget andet end migreringskæden har lagt den der. Samme forklaring dækker den fremadrettede reference: fandtes den tabel allerede på den maskine, hvor migreringen blev skrevet, virkede det fint at referere til den dér, og kun en database bygget strengt ud fra kæden ville nogensinde bemærke det.</p>
<p>To andre mekanismer kræver slet ingen skurk. Den ene er base-dumpet. Det er et snapshot, og er det blevet opdateret på et tidspunkt senere end de migreringer, der følger efter det, så holder det op med at være den historik, der faktisk fandt sted, når man importerer det og kører kæden igen — nu beder man gamle migreringer om at gøre det, dumpet allerede indeholder, eller om at droppe det, det aldrig har haft, hvilket er præcis tilfældet med den efterladte bogholderitabel. Den anden er opgraderinger af afhængigheder. Den migrering, der kaster en fejl i sit efterkørsels-hook, var formodentlig lovlig den dag, den blev skrevet, og en senere version af migreringsbiblioteket gjorde det kald ulovligt. Ingen kører den gamle ende af en kæde igen, så den slags ændring lander lydløst og venter på den næste, der bygger en database.</p>
<p>Og det er den generelle form på det. En migreringskæde bliver kun testet i sin forreste ende. Halen bliver kun rørt af én slags begivenhed — at nogen bygger et miljø op fra ingenting — og er det ikke sket i nogle år, er der ingen steder, noget fortæller dig, at den er rådnet.</p>
<h2 id="var-det-mig-eller-koden">Var det mig eller koden?<a class="gn-heading-anchor" href="#var-det-mig-eller-koden" aria-label="Link til dette afsnit"><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>Det, der gjorde det langsomt, var ikke nogen af de fire i sig selv. Det var, at jeg i et godt stykke tid ikke vidste, om problemet var koden eller mig. En ny person med en kommando, der fejler, har ingen mulighed for at skelne et brudt repository fra sin egen manglende viden, og standardantagelsen — den høflige, og oftest den rigtige — er, at det er en selv. Så læser man README&rsquo;en igen, kontrollerer sine versioner, smider det hele væk og starter fra en frisk klon og mister en eftermiddag på at bevise noget, der aldrig var sandt til at begynde med.</p>
<p>Da jeg først havde accepteret, at det var kæden, var metoden kedelig, og den virkede. Kør migreringerne frem til den version, hvor de stopper. Find ud af, hvad netop den havde brug for og ikke kunne få. Skriv patchen i hånden, markér den version som kørt uden at køre den, og fortsæt til næste blindgyde. Det gjorde jeg en håndfuld gange, hver runde en smule hurtigere end den før, indtil hele kæden kunne køre fra ende til anden uden at ødelægge noget.</p>
<p>Derefter pakkede jeg den resulterende liste af hacks ind i én enkelt mise-opgave, som er den eneste del, nogen anden nogensinde bør se — og grunden til, at det nu føles som en leg at starte tingen lokalt i stedet for et arkæologisk udgravningsprojekt.</p>
<h2 id="hvad-én-kommando-skjuler">Hvad én kommando skjuler<a class="gn-heading-anchor" href="#hvad-%c3%a9n-kommando-skjuler" aria-label="Link til dette afsnit"><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>Hvad jeg gjorde ved det, er ikke elegant. Én opgave, <code>mise run db</code>, starter containeren og stiller derefter databasen et enkelt spørgsmål: findes migreringstabellen? Alt det følgende hænger på svaret.</p>
<p>Er tabellen der, er det en eksisterende database: migrér den frem, kontrollér at den endte som aktuel, færdig. Er den ikke, importerer opgaven dumpet og går kæden igennem i etaper og stopper ved hver brudt migrering for at gøre i hånden præcis den ene ting, den migrering ikke kan gøre selv. Opret den efterladte bogholderitabel, så den migrering, der dropper den, lykkes. Tilføj den kolonne, den fejlkastende migrering ville have tilføjet, og markér derefter migreringen som kørt uden at køre den. Rejs en midlertidig udgave af den tabel, der endnu ikke findes, netop længe nok til, at den fremadrettede reference kan gå op, og drop den igen bagefter. Opret direkte den ene tabel, hvis migrering aldrig kan udføres. Kør så resten normalt, verificér at skemaet er opdateret, og udskriv testloginet.</p>
<p>Jeg ville hellere have rettet de fire migreringer. Det er et større stykke arbejde, end det ser ud til, for de er del af en kæde, som alle udrullede miljøer allerede har kørt, så at røre dem betyder at være sikker på, hvad hvert af de miljøer faktisk indeholder. Så det, der blev leveret, er omgåelsen — med årsagerne skrevet ned og en note over blokken om, at det hele kan slettes, når de er rettet. Grimt og dokumenteret slår elegant og opdigtet. Den fejlsituation, jeg går op i, er den næste person, der bruger to dage på at opdage alle fire forfra.</p>
<p>Resultatet er den del, jeg vil forsvare. På en maskine med intet andet end mise og Docker installeret rejser én kommando panelet, API&rsquo;et, et skema i den rigtige version og en konto at logge ind med. Det er sikkert at køre den igen. Og <code>mise run db:reset</code> smider databasen væk, så man kan bevise, at det hele virker fra tomt — hvilket er den egentlige test, og den, ingen havde været i stand til at køre.</p>
<h2 id="hvad-antallet-ganger-op">Hvad antallet ganger op<a class="gn-heading-anchor" href="#hvad-antallet-ganger-op" aria-label="Link til dette afsnit"><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>To omkostninger gentager sig ikke bare med antallet af repositories. De skalerer med det.</p>
<p>Den første er opdatering af afhængigheder. I samme uge opdaterede jeg i hånden en HTTP-klient i to frontends, et JSON-bibliotek i et tredje repository og et PDF-bibliotek i et fjerde: fire branches, fire pull requests, fire reviewrunder, og ingen blev klogere. Det sædvanlige argument for at automatisere det væk er sikkerhed. Ved enoghalvfjerds repositories er det et regnestykke.</p>
<p>Den anden er branch-topologi. Nogle repositories bærer stadig én branch til udvikling, én til staging og én til produktion, og ændringer rejser mellem dem via merges. Fra den uge har jeg en commit af min egen, hvis samlede besked lyder &ldquo;Fixed merge conflict error&rdquo;. Hver langlivet branch er endnu et sted, hvor den samme ændring findes i en lidt anden tilstand. Vi bevæger os mod én, hvor en merge deployer til staging og en udgivet release deployer til produktion, og hvor der ikke er noget tilbage at forlige.</p>
<h2 id="macheten">Macheten<a class="gn-heading-anchor" href="#macheten" aria-label="Link til dette afsnit"><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>To værktøjer står for det meste af rydningen, og ingen af dem var min idé — <a href="/da/author/jb/">vores tech lead</a> tog begge ind i <a href="/da/posts/four-products-68-repos/">sine egne første uger</a>. <a href="https://mise.jdx.dev/">mise</a> erklærer et repositorys værktøjskæde — og de kommandoer, man kører mod den — i en fil i repositoryet, og <a href="https://fnox.jdx.dev/">fnox</a> gør det samme for <em>referencerne</em> til dets hemmeligheder. Repositoryet committer pegepinde; værdierne bor i et administreret lager, der giver adgang per person, frem for i en fil, nogen indsætter til dig.</p>
<p>Android-appen fik sine Java- og Gradle-versioner fastlåst. Backendene fik Java og Maven fastlåst. Flere repositories fik en README, der var sand igen, og et CI-workflow, hvor der ikke havde været et.</p>
<p>Den, jeg bedst kunne lide, var desktopapplikationen. Den udsendes som en Windows-eksekverbar fil, og den krydsbygger nu fra Linux gennem en task defineret i den samme fil, der fastlåser dens værktøjskæde — og skjuler dermed en værktøjskæde, jeg ellers skulle lære, før jeg kunne røre en ændring på én linje.</p>
<p>Og for at være ærlig om dækningen: det er en håndfuld repositories ud af enoghalvfjerds. Det er mindre et program end en vane, et par af os bliver ved med at tage op.</p>
<h2 id="hvad-macheten-ikke-skærer-igennem">Hvad macheten ikke skærer igennem<a class="gn-heading-anchor" href="#hvad-macheten-ikke-sk%c3%a6rer-igennem" aria-label="Link til dette afsnit"><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>Alt det forkorter afstanden fra klon til kørende. Det gør absolut intet ved afstanden fra kørende til forstået.</p>
<p>Min seneste feature er en superadmin-skærm — den, vores eget personale bruger til at administrere de interne konti, der har adgang til backofficen. Vis dem, opret en, og siden i denne uge: rediger en. En formular, en tabel, et par kald.</p>
<p>Intet i repositoryet dokumenterede, hvad de kald forventede. Kontrakten fandtes godt nok, som et udgivet API-dokument uden for kodebasen, så typerne kom derfra frem for fra gætværk. En let uværdig måde at få en kontrakt på, og en langt bedre end at læse den eksisterende klientkode og håbe, at den, der skrev den, havde ramt rigtigt.</p>
<p>Så modsagde dokumentet sig selv. Ét endpoint beskrives som returnerende alle konti. Det svar, det erklærer, er en enkelt konto. Begge udsagn står i samme fil, og jeg kunne ikke afgøre, hvad der var sandt, ved at kalde tingen, fordi jeg ikke havde adgang til det miljø.</p>
<p>Så den klientkode, jeg skrev, accepterer en liste, eller et enkelt objekt, eller en tom body, og afleverer altid et array. Tre tests, én per form, og én manuel gennemgang af selve skærmen. Alternativet var at vælge den form, jeg vurderede mest sandsynlig, og fejle højt på den anden, hvilket ville have forvandlet en dokumentationstvetydighed til et driftsudfald. Formen på den kode er en tvetydighed, jeg ikke kunne opklare, og den eneste måde nogen finder ud af det senere, er, at jeg skrev det ved siden af.</p>
<p>Redigeringsformularen gav tre mere. Et adgangskodefelt, som API&rsquo;et ikke returnerer tomt, så formularen må åbne blank og sende ingenting, medmindre nogen taster en ny — at sende tilbage, hvad der kom ind, ville skrive volapyk oven i en fuldstændig fin adgangskode. Roller matchet på deres gemte identifikator, så en ændring skal bære den identifikator med sig; send den nye rolle uden den, og kontoen ender stille og roligt med to. Og en gemmefunktion, der behandler en række udeladt fra payloaden som uændret frem for slettet, så der slet ikke er nogen måde at fjerne en gennem den — hvilket er grunden til, at netop den sletteknap er slået fra i stedet for at stå og lade som om.</p>
<p>Hver af dem er én kommentar på stedet, der forklarer, hvorfor koden har den form, den har.</p>
<h2 id="hvor-agenterne-passer-ind">Hvor agenterne passer ind<a class="gn-heading-anchor" href="#hvor-agenterne-passer-ind" aria-label="Link til dette afsnit"><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>Jeg er generelt begejstret for kodeagenter, så her er endnu en commit fra samme uge. Dens samlede besked lyder &ldquo;revering claude slop&rdquo; — min stavefejl, min commit, der ruller tilbage, hvad en agent havde produceret.</p>
<p>Den commit svækker ikke begejstringen så meget som den placerer den. En agent er kun så god som den grund, den står på. Peg en mod et repository med en klar struktur og en fastlåst værktøjskæde og tests, der faktisk kører, og resultatet er som regel noget, man kan arbejde med. Peg en mod et repository, hvor intet af det gælder, og den gætter, flydende og selvsikkert, og så bruger man eftermiddagen på at finde ud af, hvilke dele der var gæt.</p>
<p>Det betyder, at værktøjsarbejdet og AI-arbejdet ikke er to projekter. Hver fastlåst version og hver ærlig README hæver gulvet under alt, hvad en agent gør i det repository bagefter, og de repositories, hvor jeg stoler mest på en agent, er uden undtagelse dem, nogen allerede har ryddet op i.</p>
<h2 id="to-afstande">To afstande<a class="gn-heading-anchor" href="#to-afstande" aria-label="Link til dette afsnit"><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>Afstanden fra klon til kørende er et værktøjsproblem, og den lader sig presse smukt sammen. Den går fra en eftermiddag til én kommando, der staves ens alle steder, og den presses sammen igen for den næste, der kommer.</p>
<p>Afstanden fra kørende til forstået lader sig ikke presse sammen. Ingen konfigurationsfil vil fortælle dig, at et endpoints beskrivelse og dets skema er uenige, eller at en migrering fra år tilbage holder din database som gidsel over en tjeneste, den ikke kan nå. Hvad der hjælper der, er langsommere og mindre tilfredsstillende: læs API&rsquo;ets egen beskrivelse af sig selv frem for koden, nogen har skrevet mod det, og når du ender med at skrive defensiv kode, så skriv ned, hvad du forsvarede dig mod.</p>
<p>Den anden afstand er størstedelen af arbejdet. Værdien af strømlinet værktøj er ikke, at det gør arbejdet mindre. Det er, at man ankommer til det virkelige arbejde med sine tyve minutter og sin tålmodighed i behold — og at vejen ind er ved at være den samme i hvert repository.</p>
<p>Og hvad angår opgaven: alt det, jeg pegede mig selv mod, kom i sidste ende op, og flere af dem først efter at være blevet repareret. Det er den del, jeg ikke havde regnet med ved en onboarding-opgave. Den ser ud, som om den findes for at lære den nye person landskabet, og det gør den også. Hvad den i virkeligheden producerer, er en liste over alt det, der stille og roligt var holdt op med at virke, mens ingen så efter.</p>
<p>Og det er, som jeg forstår det, netop opgaven. Johann, vores tech lead, og jeg kom til med en enkelt dags mellemrum — til en virksomhed, hvis produkt virker, og hvis tekniske fundament er vokset én applikation ad gangen, fordi det var det, den måde at bygge på lagde op til. At bringe struktur ind i det og få det til at skalere ud over den størrelse, vi har nu, er det, vi to er her for at gøre. Ikke som et oprydningsprojekt med en slutdato, men som det blivende arbejde. Alt det ovenfor er uge ét af det.</p>
<p>Og ambitionen bag det er ikke oprydning. Vi vil bygge den motor, der kan bære produktet ud i verden — den software, golfklubber kører på, i så mange lande som vil have os — og det eneste, der nogensinde giver en position som den, er kunder, der bliver forbløffede over det, de får. Struktur og skalerbarhed er måden, hvorpå man bliver i stand til at levere det, når der er ti gange så mange af dem. Et fundament, der ikke kan skalere, fejler ikke højt. Det sætter blot stille og roligt et loft over, hvor godt produktet får lov at blive.</p>
]]></content:encoded>
      <category>Teknik</category>
      <category>Infrastruktur</category>
    </item>
    <item>
      <title>Fire produkter, 68 repositories</title>
      <link>https://build.golfnext.com/da/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/da/posts/four-products-68-repos/</guid>
      <description>To uger inde hos GolfNext. Hvad vi rent faktisk bygger, hvordan tretten års software ser ud under det, de integrationer vi startede i uge to, og et ærligt kig på de mål, jeg satte mig selv, før jeg havde skrevet under på noget.</description>
      <content:encoded><![CDATA[<p>Jeg startede hos GolfNext mandag den 3. august klokken 8:00. Jeg havde forestillet mig et stille kontor og tyve minutters ventetid. Størstedelen af teamet var der allerede og i godt humør. Jeg havde en cheesecake med, som vi spiste efter frokost, for jeg har været i Danmark længe nok til at vide, at det der med kage ikke rigtig er valgfrit.</p>
<p>Peter, der startede virksomheden i 2012, havde Claude åben, før jeg havde fundet mit skrivebord, og var i gang med de konti og rettigheder, jeg skulle bruge. En lille ting at lægge mærke til på en første morgen. Den fortalte mig mere end et onboarding-deck ville have gjort.</p>
<p>Det første indlæg på denne blog var et brev, jeg skrev til mig selv, før jeg havde skrevet under på noget, fyldt med forudsigelser om en virksomhed, jeg havde besøgt én gang. Her holder jeg det op mod det, der faktisk er her. Det meste af det følgende handler dog ikke om mig. Det er virksomheden, jeg gerne vil beskrive: hvad vi bygger, hvad der ligger under det efter tretten år, og hvad vi har tænkt os at gøre ved det.</p>
<h2 id="hvad-golfnext-bygger">Hvad GolfNext bygger<a class="gn-heading-anchor" href="#hvad-golfnext-bygger" aria-label="Link til dette afsnit"><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>Hvis du aldrig har tænkt over, hvilken software en golfklub kører på, ser det sådan her ud. En klub er en lille virksomhed med flere kasketter på én gang. Der er en proshop, en driving range, medlemskaber, et bookingsystem og i stigende grad en simulator eller to. Bookingsystemet håndterer tee times, altså de starttider, hvor en gruppe er booket til at slå ud på første hul. Hele dagen i en klub er bygget op omkring det gitter.</p>
<p>Historisk kom hver af de kasketter som sin egen kasse, fra sin egen leverandør, med sin egen software vedhæftet. Køb automaten, få automatproducentens system, uanset om det passer til den måde, du driver din klub på.</p>
<p>Vores position er den modsatte. Jeres hardware og infrastruktur forbliver jeres, og I er frie til at vælge den software, der passer. Vi vil være åbne over for enhver integration, så uanset hvilket managementsystem en klub allerede bruger til sine tee times, er målet at spille sammen med det i stedet for at bede dem om at smide det ud. Frihed til at vælge, kraft til at forbinde.</p>
<p><strong>Selvbetjeningskiosker.</strong> En digital reception, der kører døgnet rundt. Greenfee, udlejning og køb i proshoppen, uden at nogen skal stå bag en disk. Så vidt vi ved er den den eneste af sin slags i golf. En klub får færre bemandede timer og tjener fortsat penge uden for åbningstiden. En spiller får den samme bekvemmelighed, som vedkommende allerede har alle andre steder i sit liv.</p>
<p><strong>Betaling på driving rangen.</strong> Betaling på rangen, der virker med alle mærker af boldautomater. Den kompatibilitet er produktet. Automaten tilhører klubben, eller den der solgte den til dem, og at kunne ligge oven på dem alle sammen er hele pointen. Kort, apps, wallets, pinkoder, med analytics bagved i realtid. Mange steder kører rangen stadig på kontanter, og det er her, efterspørgslen vokser hurtigst lige nu. Mere end 500 millioner bolde om året går igennem det. Det bliver til en hel del slicede slag.</p>
<p><strong>Voucher-håndtering.</strong> Udstedelse, distribution og indløsning, alt sammen digitalt, fra en enkelt billet til en hel sponsorpakke.</p>
<p><strong>Simulator-håndtering.</strong> Automatiseret adgangskontrol, så en simulator kun kører i en betalt session. Ikke mere tidstyveri, bedre udnyttelse.</p>
<p>Hvad det løber op i i dag: 400+ anlæg, 18 lande, 350.000+ aktive brugere. Grundlagt i Danmark i 2012 og stadig meget dansk, hvilket også er grunden til, at jeg gik hjem fra min første dag fast besluttet på at tage mit danske alvorligt.</p>
<h2 id="stakken-uden-pynt">Stakken, uden pynt<a class="gn-heading-anchor" href="#stakken-uden-pynt" aria-label="Link til dette afsnit"><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>Otteogtres repositories. JavaFX på desktopsiden, PHP, Vue, React, React Native, Spring, Java, Kotlin. Det hele på AWS.</p>
<p>Skrevet ned på den måde ligner det ubeslutsomhed, og det er det ikke. Tretten år, fire produktlinjer, hardware ude i marken, og i det meste af den historie blev softwaren bygget af eksterne partnere, der var gode til deres arbejde. Hvert projekt valgte det, der gav mening den dag, det startede, og de fleste af de valg var fornuftige. Det, der aldrig skete, er, at nogen gik tilbage bagefter og fik valgene til at stemme overens.</p>
<p>Så, den ærlige tilstand, for jeg skrev i brevet, at jeg ville skrive den slags ned i stedet for at pudse det af.</p>
<p>En pæn del af de otteogtres repositories var ikke blevet rørt i årevis og blev aldrig arkiveret. Forks, spikes, engangsværktøjer, der overlevede deres formål. Jeg begyndte at arkivere i den første uge, langsomt, for de fleste af dem vidste jeg endnu ikke, hvad jeg kiggede på, og ikke at vide er en langt bedre grund til at lade noget være end til at slette det.</p>
<p>I koden kan man læse generationerne. Flere pakkestrukturer inden for det samme sprog, ingen af dem forkerte, alle sammen forskellige. En <code>AGENTS.md</code> skrevet til Codex og en <code>CLAUDE.md</code> skrevet til Claude, der ligger i det samme repository, beskriver det samme projekt og er uenige om det. En <code>README.md</code>, der dokumenterer en version af systemet, som ikke findes længere. En runtime, der gik end of life for et stykke tid siden og stadig kører. Og en release-proces, der er rigtig god for nogle af vores værktøjer og en snowflake for andre, hvor en del af den ligger i en pipeline og en del af den ligger i hovedet på nogen.</p>
<p>Sikkerhed ligger øverst i bunken. Af de niogtyve punkter om engineering practices, jeg har skrevet ned indtil videre, er fem markeret med høj prioritet, og alle fem handler om sikkerhed. Det er ikke en opdagelse om GolfNext specifikt. Det er sådan tretten års levering plejer at se ud alle steder, hvor ingen for nylig er blevet betalt for at gå og kigge efter.</p>
<p>Da jeg lavede en forudsigelse om det hele udefra, skylder jeg dig resultatet. Det ligger nogenlunde, hvor jeg regnede med, og det mener jeg som et fair udfald snarere end en klage. Det her er tretten års kontinuerlig levering fra folk, der sendte funktioner ud, som kunderne rent faktisk havde brug for, på deadlines, med de prioriteter de fik stukket i hånden. Man ville finde nogenlunde det samme billede i de fleste virksomheder på den alder, og jeg vil hellere være den, der rydder op, end den, der sukker over det. Den ene forudsigelse, der ramte præcist, var den kedelige: jeg gættede udefra på, at repoerne ville være underautomatiserede, og det er de. Det er min åbning.</p>
<h2 id="det-jeg-tog-fejl-af">Det, jeg tog fejl af<a class="gn-heading-anchor" href="#det-jeg-tog-fejl-af" aria-label="Link til dette afsnit"><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>Jeg havde gjort mig klar til, at produkterne ville være det svage punkt. Virksomheder med en stak som vores har ofte &ldquo;sendt-alligevel&rdquo;-software ovenpå, den slags hvor man kan mærke deadlinen i brugerfladen.</p>
<p>Sådan er det ikke her. Produkterne er gode. Der er plads til forbedring i dem alle sammen, selvfølgelig. Men det, jeg bliver ved med at lægge mærke til i møder, er, at alle tænker fra kundens side først, også folk uden nogen teknisk baggrund overhovedet. Hvad oplever spilleren, der står ved kiosken klokken halv seks om morgenen. Hvad skal en klubmanager forklare et medlem, når noget går galt.</p>
<p>Det instinkt er svært at eftermontere i et team. Pakkestrukturer kan refaktoreres og README-filer kan skrives om, og begge dele er bare arbejde. At lære en gruppe mennesker at bekymre sig om personen i den anden ende er et problem af en helt anden slags, og det har vi ikke.</p>
<h2 id="det-vi-startede-i-uge-to">Det vi startede i uge to<a class="gn-heading-anchor" href="#det-vi-startede-i-uge-to" aria-label="Link til dette afsnit"><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>To integrationer landede næsten med det samme, og de er det samme væddemål i forskelligt tøj. Platformen er kun åben, hvis den rent faktisk forbinder til de systemer, klubberne allerede kører.</p>
<p><strong>teeControl</strong> er et managementsystem, som mange klubber bruger, og vi integrerer det, så det virker gennem vores kiosker. Førstehåndsindtrykket er godt. Førstehåndsindtryk er også billige, og der er et stykke vej herfra. Vi har brug for ordentlige testsystemer. Vi kommer sandsynligvis til at skulle justere nogle af vores egne flows, så de passer til deres. Der er stadig beslutninger, der skal falde på plads i begge ender. Det bliver en proces snarere end en sprint, og det siger jeg hellere nu end at skrive et triumferende indlæg om det i uge to.</p>
<p><strong>Lightspeed</strong> er en spillervendt app, som vi gerne vil understøtte som betalingsmulighed. Peter havde allerede bygget en første version, før jeg kom, og at overtage den var min anledning til at få fingrene i rigtig kode her for første gang. Ti tusind linjer, læst fra ende til anden, og derefter kørt og rodet med, indtil jeg forstod, hvad den lavede. Det gav en liste af fejl og en stak opfølgende stories, hvilket er det, der kommer ud af at læse hvad som helst grundigt. Det mere nyttige resultat er, at den nu også ligger i mit hoved og ikke kun i hans.</p>
<p>Og <strong>Verifone</strong>-terminaler på skrivebordet, som er den jævnlige påmindelse om, at en stor del af vores software ender i en fysisk genstand, som nogen skal røre ved.</p>
<p>Den tilgang, <a href="/da/author/jn/">John</a> og jeg landede på, er at springe den store forudgående specifikation over. John er den anden udvikler, selvom det ikke yder ham retfærdighed, for han er lige så meget løsningsarkitekt og projektleder som ingeniør. Få en testinstans, byg op mod den, lad virkeligheden skrive dokumentationen. To uger inde kender jeg ikke systemerne godt nok til at skrive en god specifikation, og at lade som om ville bare producere et dokument, vi stille og roligt ville holde op med at læse i september.</p>
<p>En lille ting, der er værd at give videre til enhver, der overvejer at arbejde et sted som her. Jeg brugte mine første par dage på at være tålmodig omkring adgange, ud fra en antagelse om, at der var en proces, jeg skulle vente på. Så sagde jeg højt, at det her stort set var det, jeg var blevet ansat til, og om jeg måtte få dem nu. En halv time senere havde jeg AWS. Ventetiden var udelukkende min egen opfindelse.</p>
<h2 id="hvordan-vi-udruller">Hvordan vi udruller<a class="gn-heading-anchor" href="#hvordan-vi-udruller" aria-label="Link til dette afsnit"><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>Det mest interessante problem i de første to uger lå ikke inde i et repository. Det var at finde ud af, hvordan ændringer kommer ud til kunderne.</p>
<p>Jeg brugte en fredag på ordentligt detektivarbejde. CloudWatch-logs på den ene skærm, et downloadet database-snapshot på den anden, og transaktionstilstande stillet op mod det, koden mener burde være sket. Et sted derinde fandt jeg fyrre commits, der aldrig var blevet pushet nogen steder. Det er den ucharmerende halvdel af at hente viden hjem. Adgange er den nemme del. At genopbygge en forståelse af, hvad systemet rent faktisk gør, tager meget længere tid.</p>
<p>Vi har ikke nogen gradvis udrulning i dag. Det, vi har, er kunder, der er fysiske steder, og en håndfuld af dem ligger en kort køretur fra kontoret. Så den første ring er geografi. Nogle få nærliggende klubber får den nye version først, og hvis det går galt, er redningsplanen tyve minutter i bil med en reserveterminal i bagagerummet. Det tog omkring fem minutters samtale at nå frem til, for når man først lægger mærke til, hvor kunderne er, er det et ret oplagt træk. Det er ikke det, jeg ville tegne på en tavle. Det er tilgængeligt i denne uge, og det køber tillid nok til at blive ved med at levere, mens det ordentlige bliver bygget.</p>
<h2 id="at-lære-spillet-og-ordene-for-det">At lære spillet og ordene for det<a class="gn-heading-anchor" href="#at-l%c3%a6re-spillet-og-ordene-for-det" aria-label="Link til dette afsnit"><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>På dag tre spillede jeg den første golfrunde i mit liv, sammen med kolleger. Syv eller otte slag per hul. Aldrig ti. Vi mistede ikke en eneste bold hele runden, hvilket jeg får at vide er den mere imponerende statistik for en begynder. Ingen skrev en score ned.</p>
<p>Jeg vil anbefale den generelle version af det til enhver, der starter i en virksomhed, hvis domæne de ikke kender. Ikke golf specifikt. At gå ud og gøre det, softwaren handler om.</p>
<p>Ugen efter satte John og jeg os ned til det, der blev de mest nyttige to timer i uge to: at blive enige om, hvad vi kalder tingene, før tre systemer hver opfinder deres eget ord for det samme begreb. Ni huller er en loop. Stroke index rangerer hullerne efter sværhedsgrad. Der er rating category. Intet af det er eksotisk, hvis man spiller golf, og det er alt sammen usynligt for en udvikler, der aldrig har stået på et teested, hvilket for to uger siden var mig.</p>
<p>At få ordforrådet rigtigt tidligt koster næsten ingenting. At få det forkert koster dig tre services, tre navne for én ting og et oversættelseslag, ingen havde tænkt sig at bygge. Jeg kørte ét tjek, som jeg vil anbefale enhver, der gør det her: jeg fik Claude til at verificere &ldquo;loop&rdquo; på egen hånd, fordi det lugtede for mig af et ord, udviklere opfinder og derefter overbeviser sig selv om er domænesprog. Det kom tilbage som rigtig golf. At bruge modellen til at tjekke vores eget ordforråd, i stedet for til at skrive kode, er en af de bedre anvendelser, jeg har fundet til den indtil videre.</p>
<h2 id="niogtyve-punkter-og-hvor-det-her-er-på-vej-hen">Niogtyve punkter, og hvor det her er på vej hen<a class="gn-heading-anchor" href="#niogtyve-punkter-og-hvor-det-her-er-p%c3%a5-vej-hen" aria-label="Link til dette afsnit"><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>Forbedringslisten ligger i ClickUp nu, mest så jeg kan holde op med at bære rundt på den i hovedet. Niogtyve punkter, alle stadig i backloggen, groft grupperet i repository-standarder og konventioner, sikkerhed og hemmeligheder, CI/CD og test og afhængigheder, infrastruktur og data, arkitekturretning, og AI og agenter.</p>
<p>Det første, vi rent faktisk har rullet ud, er det mindst spændende punkt på hele listen. <a href="https://mise.jdx.dev/">mise</a>, som fastlåser og installerer de rigtige sprogversioner per projekt, så man kloner et repo, og den korrekte Java dukker op af sig selv. Med så mange teknologier spredt over så mange repositories er den skat, man betaler hele dagen, at skifte mellem dem, og netop den skat kan undgås.</p>
<p>Målet har ikke ændret sig siden brevet. Byg et landskab, hvor kontekst er billig at samle op. Hvis en ny udvikler, eller en frisk AI-session, hurtigt kan forstå en service, kan arbejdet gå i gang med det samme, af hvem der end tager fat. Hvis de ikke kan, starter hver opgave med arkæologi. Alt på den liste med niogtyve punkter ligger nedstrøms af den ene sætning.</p>
<h2 id="scorekortet">Scorekortet<a class="gn-heading-anchor" href="#scorekortet" aria-label="Link til dette afsnit"><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>Hvilket bringer mig tilbage til brevet og løftet om at rette mine egne opgaver offentligt.</p>
<p><strong>Bliv ved tasterne.</strong> Holdt. To terminalrettelser sendt ud, pull requests reviewet, og jeg har overtaget ejerskabet af en af apps&rsquo;ene. Jeg bygger stadig, hvilket var den betingelse, jeg satte, før jeg takkede ja til jobbet.</p>
<p><strong>Genopbyg forståelsen, ikke bare adgangen.</strong> For det meste. CloudWatch-eftermiddagen, database-snapshottet, Lightspeed-overdragelsen: det er den rigtige form for arbejde. Det, jeg endnu ikke kan påstå, er at kunne frigive hvad som helst i stakken uden at spørge nogen.</p>
<p><strong>Åbenhed i det åbne.</strong> Holdt, og lettere end jeg havde regnet med, mest fordi rummet her gør det let.</p>
<p>Nu den anden halvdel.</p>
<p><strong>Mål nummer et skred.</strong> &ldquo;Gør arbejdet synligt&rdquo; stod øverst på listen, og denne blog var måden. Hvert eneste indlæg før det her blev skrevet, før jeg havde skrevet under på kontrakten, hvilket viser sig at være det nemmeste tidspunkt overhovedet at skrive om en virksomhed. Så startede jeg rent faktisk, og i to uger var der ting foran mig, der betød mere end et blogindlæg. Jeg vil ikke kalde det en krise. Jeg skrev, at hvis der blev stille, skulle jeg tage det som et advarselstegn, så: noteret.</p>
<p><strong>Koldstarten er milevidt fra fem minutter.</strong> Jeg satte mig selv fem minutter fra klon til kørende kode og bad fremtidige mig om at tage tid på det. Jeg har ikke taget ordentligt tid på det, hvilket er sin egen lille fiasko. Jeg har klonet nok af de her repositories efterhånden til at give et fair skøn. Et sted mellem to og seks timer afhængigt af hvilket, man vælger, lad os sige fire i gennemsnit. At få iOS-buildet til at køre tog det meste af en dag med Xcode, pods og emulatorer. mise er det første rigtige skridt mod at lukke det hul, og et skridt er ikke et tal.</p>
<p><strong>Det fælles AI-kit er punkt niogtyve ud af niogtyve, og den del er med vilje.</strong> I brevet kaldte jeg det den ting, der gør &ldquo;AI-first&rdquo; fra et slogan til rigtigt fælles værktøj. To uger inde kan jeg se, hvorfor det ikke kan komme først. Et fælles kit bygget oven på otteogtres inkonsistente repositories ville mest af alt bare indkode inkonsistensen. Fundament først, kit bagefter, og den rækkefølge vil jeg forsvare. Det, jeg ikke vil lade som om, er, at rækkefølgen er gratis. Indtil vi når dertil, får jeg en masse gearing ud af et setup, jeg har brugt et år på at finpudse, og som ingen andre her har endnu.</p>
<p><strong>Og dagene åd pladsen til at tænke.</strong> Jeg skrev, at den kreative, legende tid ikke er en belønning, man optjener, når det seriøse arbejde er gjort, at den <em>er</em> det seriøse arbejde. To uger inde er det det første, kalenderen tager. Én ting hjælper her, og den er specifik for den her branche: golf er sæsonbetonet. Klubberne har travlt nu, så vi har travlt nu. Platformen bliver stille hen over vinteren, og det er der, værktøjskassen til 2027-sæsonen bliver bygget. At have en form på sit år er mere nyttigt, end jeg havde regnet med.</p>
<p>Det, jeg tog mest fejl af, er også det, jeg er gladest for. Jeg mødte op forberedt på modstand. Jeg regnede med at skulle gøre mig fortjent til adgange, argumentere for omfang og føre sag for hver eneste ændring. Næsten intet af det var der, og den modstand, jeg mødte i de første fjorten dage, havde jeg selv taget med.</p>
<p>To uger er ikke længe nok til at konkludere ret meget. Spørg mig igen til vinter, når klubberne bliver stille, og vi finder ud af, hvad vi rent faktisk gør med tiden.</p>
]]></content:encoded>
      <category>Teknik</category>
      <category>Produkt</category>
      <category>Kultur</category>
    </item>
    <item>
      <title>Få Claude Code til at tie stille (og andre tilpasninger, der har gjort sig fortjent)</title>
      <link>https://build.golfnext.com/da/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/da/posts/tuning-claude-code/</guid>
      <description>Det første, jeg gjorde med Claude Code, var at få den til at tale mindre. De små, delbare tilpasninger, der overlevede mit eget filter — kortfattet output, en statuslinje, de rigtige hooks — plus dem, jeg bevidst nægter at automatisere, og hvorfor det at styre kontekstvinduet slet ikke burde være en færdighed.</description>
      <content:encoded><![CDATA[<p>Det første, jeg gjorde med <a href="https://code.claude.com/docs">Claude Code</a>, var at få den til at tale mindre.</p>
<p>Claude Code er Anthropics terminal-kodeassistent — du taler med den, og den læser, skriver og kører kode i dit projekt. Ud af boksen er den høflig. &ldquo;Certainly! I&rsquo;d be happy to help you with that. The issue you&rsquo;re seeing is likely caused by&hellip;&rdquo; Jeg læser hurtigt, jeg betaler per token, og jeg har ikke brug for opvarmningen. Det slog jeg fra. Jeg kører et community-plugin, <a href="https://github.com/JuliusBrussee/caveman">caveman</a>, der skærer svarene ind til benet: ingen artikler, ingen høfligheder, hver eneste tekniske detalje intakt. &ldquo;Bug in auth middleware. Token expiry check uses <code>&lt;</code> not <code>&lt;=</code>. Fix:&rdquo; — og så rettelsen.</p>
<p>Det kræver tilvænning. Nogle af de komprimerede sætninger får dig til at stoppe op og læse dem to gange; et fragment kan bære mere mening, end det først lader ane, og du bruger det sparede sekund på at afkode det. Men byttet falder klart ud til min fordel. Jeg kommer hurtigere til sagen, kontekstvinduet fyldes langsommere, og hvis en linje er decideret uklar, er der altid nødudgangen: bed den om at udfolde den. Den brokker sig aldrig.</p>
<p>Den tilstand er det mindst mulige eksempel på det, hele dette indlæg handler om. Claude Code ud af boksen er generisk. Den er bygget til at være fornuftig for alle, hvilket betyder, at den er optimal for ingen. Gevinsten ligger ikke i modellen selv — du kan ikke ændre, hvor god den er, kun hvilken én du kører. Gevinsten ligger i at forme værktøjet om din egen arbejdsgang.</p>
<h2 id="fælden">Fælden<a class="gn-heading-anchor" href="#f%c3%a6lden" aria-label="Link til dette afsnit"><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>Tilpasning er ikke gratis, og jeg vil være ærlig om regningen. Du kan hælde uger i et setup, ingen anden kan læse — heller ikke dit fremtidige jeg.</p>
<p>De værktøjer, der drev konfigurerbarhed længst, er dem, alle hylder: vim og git. Dybe, kraftfulde, uendeligt indstillelige. Og, vil jeg forsigtigt påstå, ikke just særligt tilgængelige. Dybden er både pointen og prisen. Vim er det eneste værktøj, jeg kender, med et meme om, hvordan man overhovedet lukker det — det burde sige os noget.</p>
<p>Jeg hører indvendingen oftest om git: &ldquo;men git er da så <em>nemt</em>.&rdquo; Er det nu det? Det er nemt — lige indtil din første rigtige merge-konflikt, eller eftermiddagen, hvor du rebaser det forkerte og stille skriver din egen historik om uden nogen ren vej tilbage. Ja, du kan godt se, hvor du prøver at komme hen. Det kan ham, der sidder med æsken til et puslespil på halvtreds tusind brikker, også — med det færdige billede trykt lige der på låget. At se målet er ikke det samme som, at vejen er tilgængelig. Det er to forskellige påstande, og vi bliver ved med at blande dem sammen.</p>
<p>Her er overbevisningen, sagt rent ud i stedet for som en klage: et godt værktøj bør være så tilgængeligt, som det overhovedet kan være — og <em>derefter</em>, når det har gjort sig fortjent, tilbyde uendelig dybde til enhver, der vil grave. Den tilgængelige del er hele opgaven, ikke en trøstepræmie til begyndere. Dybden er bonussen, du lægger ovenpå.</p>
<p>Min målestok for enhver Claude Code-tilpasning er ét spørgsmål: <em>ville jeg kunne række det til en kollega uden en times briefing?</em> Hvis svaret er nej, overlever det sjældent. Min stak holdes bevidst simpel og dum. Her er, hvad der er tilbage efter det filter.</p>
<h2 id="hvad-fortjente-en-plads">Hvad fortjente en plads<a class="gn-heading-anchor" href="#hvad-fortjente-en-plads" aria-label="Link til dette afsnit"><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>Den kortfattede output-tilstand.</strong> Dækket ovenfor. Den enkeltvis mest virkningsfulde ændring, fordi den rører hvert eneste svar. Mindre at læse, færre tokens brændt, længere levetid for konteksten.</p>
<p><strong>Statuslinjen.</strong> Den betyder mere, end den lyder til. Jeg har som regel flere ting i gang på én gang og mister overblikket over, hvor jeg er — hvilket repo, hvilken opgave, hvor meget budget jeg har tilbage. Min statuslinje viser det hele i én stribe: modellen, jeg kører på, hvor fyldt kontekstvinduet er, hvor jeg står i forhold til min 5-timers forbrugsgrænse (Claude Code sætter et loft over, hvor meget du kan køre inden for et rullende fem-timers vindue), og min nuværende mappe. Ingen klik, ingen gætteri. Jeg kigger ned og kender min situation. For enhver, der jonglerer mere end én ting ad gangen, er det den billigste sindsro, jeg har købt.</p>
<p><strong>Indstillingslaget.</strong> Det er her, det bliver personligt, og hvor mulighederne aldrig hører op. Min konfiguration spiller en lyd, når Claude er færdig, så jeg kan kigge væk og blive kaldt tilbage. Den sender OS-notifikationer. Du kan få den til at logge formålet med hver session, holde styr på tokens og pris, hvad end du beslutter er værd at vide. Der findes ingen &ldquo;rigtig&rdquo; opsætning — pointen er, at hooks&rsquo;ene findes (små kommandoer, værktøjet kører automatisk på bestemte tidspunkter, for eksempel når det er færdigt), og at du former dem til <em>dit</em> formål. Mit er &ldquo;sig til, når du er færdig.&rdquo; Dit kunne være noget helt andet.</p>
<p>Ingen af dem er smarte. Det er netop pointen. Hver enkelt fjerner en lille, daglig friktion, og ingen af dem kræver en forklaring.</p>
<h3 id="selve-ledningsføringen">Selve ledningsføringen<a class="gn-heading-anchor" href="#selve-ledningsf%c3%b8ringen" aria-label="Link til dette afsnit"><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>Lyden er et <code>Stop</code>-hook i <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>, så den aldrig blokerer sessionen, og <code>afplay</code> er bare den indbyggede macOS-afspiller — skift den ud med hvad du vil.</p>
<p>Statuslinjen er knap nok mere end det. Claude rækker dit script en klump JSON ved hver gengivelse; scriptet printer en linje. Du peger på det én gang:</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>og scriptet læser de felter, det er interesseret i, fra 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>Derfra er det bare at tegne strenge. Min tegner en lille farvet bjælke for hver procentdel — grøn, så gul, så rød, efterhånden som den fyldes — og højrejusterer stien, så striben ender med at se sådan ud:</p>
<pre tabindex="0"><code>Opus 4.8  xhigh  · CTX ████░░░░░░ 38%  · 5h ██░░░░░░░░ 21%        ~/dev/golfnext
</code></pre><p>(<code>xhigh</code> der er bare det niveau af ræsonneringsindsats, jeg holder modellen på.) Det er hele molevitten. Ingen framework, ingen afhængighed at holde opdateret. Et shell-script og en klump JSON.</p>
<h2 id="hvad-jeg-ikke-automatiserer">Hvad jeg ikke automatiserer<a class="gn-heading-anchor" href="#hvad-jeg-ikke-automatiserer" aria-label="Link til dette afsnit"><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>Mere automatisering er ikke altid en gevinst. Noget af det flytter bare omkostningen hen et sted, du ikke kan se, og dem lader jeg være slået fra.</p>
<p>Du kan få Claude til at linte og formatere hver fil i samme øjeblik, den redigerer den. Lyder ryddeligt. Det er det modsatte. Hvis et hook omformaterer en fil lige efter, modellen har rørt den, er modellens billede af den fil nu forældet — så næste gang den arbejder der, må den læse hele filen igen for at følge med. Du har byttet et rent diff for en langsommere, mere token-sulten session. Jeg vil hellere have, at den bare kører linteren én gang, til allersidst, når tænkningen er færdig.</p>
<p>Samme logik gælder automatisk model-fallback. Du kan få den til stille at skifte til en mindre model, når dit første valg er optaget. Det vil jeg ikke have. Jeg vælger en model bevidst, som regel fordi opgaven kræver den større ræsonneringsmodel — den, der standser op og arbejder et problem igennem, før den svarer. Hvis den er overbelastet, vil jeg hellere komme tilbage om to timer og skrive &ldquo;tag den her op igen&rdquo; end at lade noget stille række mit svære problem videre til en model, der ikke kan bære det. Bekvemmelighed er ikke et dårligere svar værd, som jeg ikke opdagede, jeg fik.</p>
<h2 id="vinduet-bør-være-en-ikke-begivenhed">Vinduet bør være en ikke-begivenhed<a class="gn-heading-anchor" href="#vinduet-b%c3%b8r-v%c3%a6re-en-ikke-begivenhed" aria-label="Link til dette afsnit"><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>Folk taler meget om at styre kontekstvinduet — komprimere det (få værktøjet til at opsummere samtalen indtil videre for at frigøre plads), holde øje med det, pleje det. Jeg prøver aldrig at være i den samtale.</p>
<p>Måden, jeg undgår det på, er strukturel: skub det tunge arbejde over på subagenter — separate hjælpe-instanser af Claude Code, hver med sin egen private kontekst — og hold hovedtråden ren. En subagent tager af sted, brænder sin egen kontekst af på at læse og søge, og rækker det korte svar tilbage. Hovedsessionen forbliver let. Gjort rigtigt rækker jeg næsten aldrig ud efter komprimering, fordi jeg aldrig lader vinduet blive fyldt i første omgang.</p>
<p>Men her er den ærlige del: jeg synes, det er en lugt i sig selv, at nogen af os overhovedet skal vide det her. Ingen burde <em>behøve</em> at forstå kontekstvinduer for at begynde at være produktiv med værktøjet. Det er en nuværende begrænsning forklædt som en færdighed. Om et par år vil jeg vædde på, at det er abstraheret helt væk, og at vi taler om det, som vi taler om at styre vores egen RAM — hvilket vil sige, at det gør vi ikke. Jeg optimerer omkring det i dag, og jeg prøver ikke at forveksle det at arbejde uden om en begrænsning med noget, der er værd at lære fra sig.</p>
<p>Kan jeg bevise det? Ikke med tal. Jeg har ikke et rent før-og-efter, og jeg er skeptisk over for enhver, der citerer en pæn procentdel for det her. Hvad jeg har, er en fornemmelse: vinduet fyldes langsommere, budgettet rækker længere, sessionerne forbliver lette længere. Det er subjektivt, og jeg ville mærke det i samme øjeblik, det var væk.</p>
<h2 id="sådan-finder-du-dine-egne">Sådan finder du dine egne<a class="gn-heading-anchor" href="#s%c3%a5dan-finder-du-dine-egne" aria-label="Link til dette afsnit"><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>Den svære del er ikke at konfigurere noget. Det er at vide, hvad der overhovedet er muligt.</p>
<p>Claude Code bevæger sig hurtigt — det føles som ti nye kommandoer om ugen. Så hurtigt, at modellen selv ikke kan følge med. Jeg spørger den om en rigtig slash-kommando — &ldquo;hvad gør <code>/goal</code>, kan du forklare det?&rdquo; — og den aner ikke, at kommandoen findes. Der er noget komisk over det: det mest kapable, jeg arbejder med, kan ofte ikke fortælle dig om funktioner, der er kommet til efter dens træningsgrænse. Den er bagud på sig selv, helt bogstaveligt. Du kan ikke bare spørge modellen, hvad den kan. Du er nødt til at gå og kigge — læs changelogen, skim en guide, peg den ud på det åbne web, hvor det friske faktisk lever.</p>
<p>Den næste ting, jeg vil prøve, er slet ikke en indstilling. Jeg har et halvt dusin projekter i gang, så jeg vil bygge ét lille meta-repo: ét sted, der peger på dem alle, og som enhver — ikke kun jeg — kan navigere i. Samme instinkt som alt andet her: hvis det kun virker for mig, virker det ikke rigtigt.</p>
<p>Det er hele filteret, når det kommer til stykket. Tag de to-tre ting, der dræber en rigtig, gentagen friktion. Lad resten ligge, uanset hvor smart det ser ud. Målet var aldrig det mest avancerede setup. Det var det, du stadig ville være glad for at række videre til en anden.</p>
]]></content:encoded>
      <category>AI</category>
      <category>Teknik</category>
    </item>
    <item>
      <title>Loop-debatten er et dårligt ord for autonomi</title>
      <link>https://build.golfnext.com/da/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/da/posts/loops-autonomy/</guid>
      <description>Alle diskuterer &#34;Claude Code loops.&#34; Efter at have målt en måneds rigtige opgaver — hver token, hver time det ville have kostet i hånden — er jeg ret sikker på, at det meste af diskussionen handler om det forkerte ord. Det er ikke loops, det er autonomi. Og ligesom microservices for ti år siden ankommer det én verificerbar opgave ad gangen, begrænset af et fundament, ingen har bygget endnu.</description>
      <content:encoded><![CDATA[<p>Der er en genre af indlæg, der florerer lige nu. Du har sikkert set dem. &ldquo;Jeg skriver ikke prompts længere — jeg designer loops.&rdquo; &ldquo;Loop-engineering er det nye prompt-engineering.&rdquo; Jeg læser dem grundigt — jeg prøver at lære det her lige så hurtigt som alle andre, og dem, der skriver dem, er som regel langt foran mig. Men jeg sidder hver gang tilbage med den samme kløe: jeg kan ikke helt få ordet til at passe på det, jeg faktisk laver hele dagen.</p>
<p>Jeg bruger Claude Code de fleste dage. For dem, der ikke har mødt det: Claude Code er Anthropics kodeagent — du giver den en opgave i terminalen, og den læser filer, skriver kode, kører tests og itererer, i stedet for bare at række dig et stykke kode, du selv skal indsætte. Evnen er reel, og jeg læner mig hårdt op ad den. Det, der spænder ben for mig, er ordforrådet, så jeg prøvede at jorde det.</p>
<p>Jeg gjorde det kedelige: jeg målte alt. Hver rigtig opgave, jeg kørte gennem Claude Code, fik to tal ved siden af sig — hvad den kostede i tokens, og et groft estimat af, hvad det samme arbejde ville have kostet i hånden, i tid. En måneds rækker. Det er det mest uglamourøse artefakt, man kan forestille sig, og det er det, der til sidst fik &ldquo;loop&rdquo;-debatten til at falde på plads for mig.</p>
<p>Det, der faldt på plads — i hvert fald for mig — er, at en stor del af diskussionen handler om det forkerte ord. &ldquo;Loop&rdquo; er ikke sagen. Autonomi er. Og autonomi kommer ikke som en kontakt, du vipper; det kommer én opgave ad gangen — hvilket, hvis du var med under sidste store arkitektur-diskussion, burde føles besynderligt bekendt.</p>
<h2 id="loop-er-fire-forskellige-ting">&ldquo;Loop&rdquo; er fire forskellige ting<a class="gn-heading-anchor" href="#loop-er-fire-forskellige-ting" aria-label="Link til dette afsnit"><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>En del af grunden til, at debatten føles grumset, er, at &ldquo;loop&rdquo; udfylder mindst fire roller på én gang, og folk taler forbi hinanden, fordi de hver især holder en forskellig en.</p>
<p>Der er <strong>agent-loopet</strong> — tænk, handl, observér, gentag. Det er bare sådan en agent virker. Den læser situationen, foretager en handling, ser på resultatet, beslutter hvad der er næste skridt. Det har altid eksisteret. Det er ikke en teknik, det er definitionen.</p>
<p>Der er <strong>Ralph</strong> — bash-hacket. Det er fællesskabets navn for det groveste tænkelige loop: pak agenten ind i et bogstaveligt <code>while</code>-loop i din shell, og lad den køre den samme prompt igen og igen, indtil den selv erklærer sig færdig. (Navnet er bare folklore; teknikken er pointen.) Groft, til tider effektivt — autonomiens gaffatape.</p>
<p>Der er <strong>funktionerne</strong> — slash-kommandoer som <code>/loop</code> og <code>/goal</code>, indbygget i værktøjer som Claude Code. De pakker den samme idé ind i noget mere kontrolleret end en bash-onelinjer.</p>
<p>Og der er <strong>&ldquo;loop-engineering&rdquo;</strong> — filosofien. Diskursen. Indlæggene.</p>
<p>Fire forskellige højder, ét ord. Ikke så sært, at det forvirrer.</p>
<p>Men her er det, de alle deler, sætningen hver af dem gentager på sin egen dialekt: <strong>mål, verifikation, stopbetingelse.</strong> Den trio er den egentlige substans. Alt andet er bare et spørgsmål om, hvilket lag den enkelte tilfældigvis stirrer på.</p>
<p>Lad os gøre det konkret. Tag den mest kedelige nyttige ting, jeg automatiserer: at adressere review-kommentarer på en pull request.</p>
<ul>
<li><strong>Mål:</strong> løs alle review-kommentarer på denne PR.</li>
<li><strong>Verifikation:</strong> CI er grøn, og hver kommentar-tråd er reelt adresseret — ikke &ldquo;jeg tror, jeg fiksede det,&rdquo; men checket består, og tråden er lukket.</li>
<li><strong>Stopbetingelse:</strong> ingen åbne kommentarer tilbage, eller den rammer et iterationsloft og giver bolden tilbage til mig.</li>
</ul>
<p>Det er et loop. Det er også bare en opgave med en klar definition af færdig og en regel for, hvornår man stopper. Trioen er hele spillet.</p>
<p>Og læg mærke til, hvad der får eksemplet til at virke: en maskine, ikke mig, kan afgøre, om målet er nået — checket er grønt, eller det er ikke. Det er det, jeg fremover vil kalde <em>verifikation</em> — et automatisk, dømmefrit signal om, at arbejdet faktisk er gjort. Hold fast i ordet. Hvor meget af det en opgave har, er det, der afgør alt det senere.</p>
<h2 id="det-er-ikke-loops-det-er-autonomi">Det er ikke loops. Det er autonomi.<a class="gn-heading-anchor" href="#det-er-ikke-loops-det-er-autonomi" aria-label="Link til dette afsnit"><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>Her er den del, det tog mig et stykke tid at se. Læs de tre kriterier igen — mål, verifikation, stopbetingelse — og spørg: ændrer noget af det sig, alt efter om <em>jeg</em> trykker enter mellem hvert skridt, eller en harness (scriptet eller værktøjet, der omslutter agenten) gør det for mig? Nej. Intet. Målet er målet, verifikationen er verifikationen, stopbetingelsen er stopbetingelsen; de er identiske, uanset om et menneske rykker hver iteration frem, eller et script gør det. Den eneste variabel er <strong>hvem der drejer håndsvinget mellem skridtene</strong> — og det er slet ikke et spørgsmål om loops. Det er et spørgsmål om autonomi: hvor meget du stoler på, at tingen kører videre uden dig i stolen.</p>
<p>Hvilket giver mening, når man ser, hvor lidt der egentlig er ved et loop — nogenlunde sådan her:</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>Det er det hele. Så &ldquo;bør jeg designe loops&rdquo; er lidt af en kategorifejl — loopet er trivielt, et <code>while</code> og et <code>if</code>. De svære dele gemmer sig bag ordet, og der er to af dem. Den første: hvor langt er du villig til at træde væk fra en kørende proces? Det er tillid. Den anden, den jeg næsten aldrig ser nævnt: når du <em>først</em> træder væk, hvor kører tingen så rent faktisk, og har du råd til at køre den der? Det er infrastruktur — og i modsætning til tillid er det ikke rigtig op til dig endnu, fordi det billige, standardiserede sted at køre autonome agenter ikke findes.</p>
<h2 id="hvad-arbejdet-faktisk-koster">Hvad arbejdet faktisk koster<a class="gn-heading-anchor" href="#hvad-arbejdet-faktisk-koster" aria-label="Link til dette afsnit"><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>Det andet spørgsmål — har du råd til at køre det — er det, ingen lader til at blogge om, sandsynligvis fordi næsten ingen tænker på at måle det. Så det gjorde jeg. Min hverdag med Claude Code er overvåget — jeg kører det på min egen maskine, med mig selv i stolen. Men det tal, jeg var ude efter, var det andet: hvad det samme arbejde ville koste <em>målt pr. forbrug</em>, betaling pr. token, sådan som du ville være nødt til at betale for at køre det i skyen uden mig som barnepige. Det er den dyre bane, og det er den, der afgør, om uovervåget overhovedet er på bordet. Så i en måned loggede jeg den forbrugsmålte pris for hver rigtig opgave, ved siden af et ærligt estimat af, hvor lang tid det ville have taget mig i hånden.</p>
<p>Tohundrede og tyve opgaver. <strong>Omkring $1.360 i forbrugsmålt pris — og, efter mit eget estimat, tæt på 540 timers tilsvarende håndarbejde.</strong> Femhundrede og fyrre timer er den bedre del af tre måneders fuldtids-ingeniørarbejde. Det røg ud ad døren på én.</p>
<p>Sæt et hvilket som helst rimeligt tal på en ingeniørtime, og de forbrugsmålte $1.360 står over for arbejde til <strong>tyve til fyrre gange</strong> så meget — omkring tredive til en normal fuldt belastet timepris, hvor man tæller løn plus overhead og ikke bare det, der ryger på kontoen. Multiplikatoren behøver ikke være præcis: halvér mine timeestimater, hvis du synes, de er rundhåndede, og det er stadig en størrelsesorden.</p>
<p>Formen på forbruget betyder lige så meget som størrelsen. Median-opgaven kostede <strong>$2,45</strong>. To tredjedele af alt lå under fem dollars — bugfixes, små features, skrøbelig CI-konfig, dependency-bumps. De dyre kørsler er sjældne, og det er de tunge af slagsen: en hel feature, bygget fra ende til anden, koster nogle tiere af dollars, og de største enkeltrækker er features i <strong>$40–55</strong>-lejet. Læg flere runder review oven på en af dem, og en komplet ændrings-livscyklus kan runde $100. Det er undtagelserne, ikke normen.</p>
<p>Den del, der afgør, hvor man skal starte: jeg markerede de rækker, der er rent review-og-fix-arbejde — at adressere PR-kommentarer, fikse et fejlende build, løse merge-konflikter. <strong>Hele den kategori lå under en tiendedel af det samlede forbrug</strong> — de fleste opgaver fra et par cents til et par dollars, halvdelen under en dollar. Og den krævede sjældent meget af mig. Kommentarerne er afgrænsede, fiksen er som regel lokal, og verifikationen er allerede sat op: buildet er grønt, eller det er ikke. Det er den mest checkbare, mindst dømmekrævende del af cyklussen — hvilket gør den til det oplagte første sted at lade noget køre på egen hånd.</p>
<p>Den reelle pengelækage er ikke prisen på den enkelte opgave. Det er churn — kørsler, der brændte krudt af på noget, der ikke havde brug for det. En ændring, der burde have kostet tyve dollars, kostede hundrede, fordi jeg lod den male løs på et problem, jeg bare burde have styret den igennem. Det spild, ikke prisen pr. opgave, er det, der er værd at lave ingeniørarbejde imod.</p>
<h2 id="det-skræmmende-tal-er-mest-af-alt-en-måle-artefakt">Det skræmmende tal er mest af alt en måle-artefakt<a class="gn-heading-anchor" href="#det-skr%c3%a6mmende-tal-er-mest-af-alt-en-m%c3%a5le-artefakt" aria-label="Link til dette afsnit"><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>Der findes en version af det her, hvor nogen ser den månedlige total og får et chok. &ldquo;Brugte du <em>hvor meget</em> på en AI?&rdquo; Og jeg forstår det godt — $1.360 om måneden er et oprigtigt alarmerende tal.</p>
<p>Men det er mest af alt en artefakt af, hvordan det opgøres, ikke af, hvad arbejdet er værd — og det er ikke engang det, jeg betaler. De $1.360 er det forbrugsmålte tal, jeg målte. Min faktiske regning er det faste abonnement, jeg kører det hele på: en &ldquo;Max&rdquo;-plan i mellemlaget, brugt i et normalt tempo — ikke et hamr-alt-igennem-modellen, lad-mig-lige-google-det-med-Claude-også-tempo, bare støt rigtigt arbejde — som løber op i omkring €110 om måneden, lad os sige $120. Samme tastetryk, samme output, samme måneds arbejde; det forbrugsmålte tal er cirka ti gange abonnementet, udelukkende på grund af hvordan det afregnes.</p>
<p>Og her er den del, der stille og roligt afvæbner hele pris-panikken: du arbejder <em>overvåget</em> alligevel. Du sidder der. Du styrer. Det dyre, ubegrænsede &ldquo;åh gud, hvad laver den&rdquo;-scenarie er det <strong>uovervågede</strong> — agenten, der maler løs på egen hånd, mens måleren spinner. Hvilket er præcis det, du er mindst klar til at stole på i første omgang.</p>
<p>Så de to store bekymringer — &ldquo;det er for dyrt&rdquo; og &ldquo;jeg kan ikke lade den køre selv endnu&rdquo; — viser sig at være den samme grænse set fra to sider.</p>
<h2 id="så-hvad-bygger-man-egentlig">Så hvad bygger man egentlig?<a class="gn-heading-anchor" href="#s%c3%a5-hvad-bygger-man-egentlig" aria-label="Link til dette afsnit"><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>Ikke et autonomt loop — ikke endnu, og ikke til det meste af det. Det, jeg bygger i stedet, er en <em>menneske-udløst kæde af skills</em>, hvor &ldquo;skill&rdquo; bare betyder en navngiven, genbrugelig opgave, du kan række agenten, som en gemt kommando. En af mine passer pull requests: når jeg har åbnet en PR, sætter jeg den i gang, og den holder øje med de ting, der kræver en reaktion — en ny runde review-kommentarer, et fejlende build — og arbejder dem, indtil PR&rsquo;en er grøn, eller den rammer noget, den ikke kan håndtere, og prikker mig på skulderen. Jeg starter den; den starter ikke sig selv.</p>
<p>Det interessante er, hvad der <em>ikke</em> kører modellen. Overvågningen er et almindeligt shell-loop, der poller GitHub hvert par minutter for en håndfuld billige signaler — åbne, uafklarede kommentar-tråde, CI-status, om PR&rsquo;en er blevet godkendt — og den vækker kun agenten, når det sæt rent faktisk ændrer sig:</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>Agenten spinder kun op, når sættet ændrer sig, og selv da gør den det afgrænsede — læser de nye tråde, fikser de værdige i arbejdstræet, kender forskel på et &ldquo;min kode er i stykker&rdquo;-build og et &ldquo;infrastrukturen væltede&rdquo;-build — og lægger sig så til at sove igen. Den dyre del, modellen, sidder bag den billige del, et script der ved, hvordan man læser en status. Den gating er det meste af tricket til at holde de her ting overkommelige.</p>
<p>Min er den pragmatiske udgave, dog, ikke evangeliet. Peg den mod en andens repo, og den har brug for rigtige guardrails. En PR-kommentar er upålideligt input, hvilket gør en åben watcher til en prompt-injection-flade: nogen kan efterlade en kommentar, der styrer agenten mod noget, der intet har med PR&rsquo;en at gøre, eller bare spamme kommentarer for at blive ved med at vække den og stille og roligt drive regningen op. Forbi legetøjsstadiet vil du have en classifier, der afgør, hvad der overhovedet er værd at reagere på, og en eskaleringsvej — en OS-notifikation, et Slack-ping, en ticket — til de ting, den ikke selv bør røre. Men man kommer overraskende langt på de rå signaler alene; &ldquo;åbne, uafklarede kommentarer plus CI-status&rdquo; er allerede nok til at være reelt nyttigt.</p>
<p>Og formen kan generaliseres. En anden, jeg bliver ved med at ville gøre færdig: et ugentligt job, der scanner de åbne Dependabot- og Renovate-pull requests, merger og rydder op i de kedelige, hvor intet reelt skal ændres, og eskalerer resten — de bumps, der kræver en egentlig refaktorering, eller hvor et API kan brække under dig. Samme skelet hver gang: en billig trigger, et afgrænset job, og en klar linje, hvor det giver bolden tilbage til et menneske. At bygge de loops, og de små toolkits omkring dem, er stille og roligt ved at blive sin egen slags software engineering — mindre &ldquo;prompt modellen,&rdquo; mere &ldquo;design den harness, modellen kører inde i.&rdquo;</p>
<p>To regler, jeg ville hugge i sten, før jeg byggede noget af det. For det første: <strong>release er ikke et job for modellen — det er din CI/CD.</strong> Lær ikke en agent at gøre det, din pipeline allerede gør gratis, deterministisk, med et revisionsspor; det er ikke autonomi; det er at genopfinde et løst problem. For det andet: <strong>monitorering er ikke et job for modellen — det er Sentry og Grafana.</strong> Betal ikke en hjerne for at glo på et dashboard; kobl i stedet alarmen tilbage til en trigger, så agenten kun får problemet i hænderne, når noget rent faktisk er gået i stykker. Og uanset hvad du bygger, så sæt et loft på fiks-loopet — en hård iterationsgrænse plus en menneskelig &ldquo;godt nok, send det&rdquo;-port — ellers bliver den $20-fiks stille og roligt til en $100-fiks, mens du er til frokost.</p>
<p>Intet af det er forsigtighed. Menneske-i-loopet er ganske enkelt måden, man <em>starter</em> på: du løsner snoren én iteration ad gangen, efterhånden som dine egne evner skærpes og — vigtigere — efterhånden som din verifikation bliver god nok til, at du ville stole på resultatet uden at stå og kigge over skulderen.</p>
<h2 id="vi-har-haft-præcis-den-her-diskussion-før">Vi har haft præcis den her diskussion før<a class="gn-heading-anchor" href="#vi-har-haft-pr%c3%a6cis-den-her-diskussion-f%c3%b8r" aria-label="Link til dette afsnit"><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>Hvis alt det her føles bekendt, er det, fordi vi spillede hele stykket igennem for ti år siden. Det hed microservices, og jeg var med. Hurtig genopfriskning, hvis du gik glip af den æra: omkring 2014 begyndte teams at hakke store, enkeltstående applikationer — &ldquo;monolitter&rdquo; — op i mange små, uafhængigt deploybare services. Den højlydte diskussion gik på, hvor <em>små</em> de skulle være.</p>
<p>Frådet er identisk. Dengang var det ubesvarlige spørgsmål &ldquo;hvor lille er en microservice — kan den være 200 linjer?&rdquo; I dag er det &ldquo;Ralph eller <code>/goal</code>, og hvor mange agenter?&rdquo; I begge tilfælde er det et fællesskab, der størrelses-diskuterer sig igennem en ting, før nogen overhovedet ved, hvad der faktisk betyder noget.</p>
<p>Den gode praksis kom også før navnet. Vi opdelte systemer efter domæne — rene snit, ejede grænser — længe før nogen sagde &ldquo;microservices.&rdquo; Og agent-loopet plus en verifikations-harness fandtes længe, før nogen brandede det som &ldquo;loop-engineering.&rdquo; Navnet dukkede op og tog æren for arbejde, folk allerede lavede.</p>
<p>Og så lagde det sig. Det ærlige tilbageblik på microservices er ikke &ldquo;vi revolutionerede alt.&rdquo; Det er &ldquo;vi byggede stort set videre, som vi allerede byggede, bare med renere snit.&rdquo; De fleste teams gik fra en monolit til to eller fem services, ikke fra én til to hundrede natten over. Forandringen var reel, og den var udramatisk, fordi det meste af det, vi byggede, allerede var småt.</p>
<p>Men her er korrektionen, der faktisk betyder noget, den del størrelses-diskussionen overså dengang. For microservices var den reelle forskel aldrig mønsteret. Det var <strong>fundamentet.</strong> Containere, så skyen, så Kubernetes gjorde små, uafhængigt deploybare services billige og standardiserede at køre. Mønsteret lå nedstrøms for infrastrukturen — og infrastrukturen dukkede op for at møde det.</p>
<p>For loops er vi kun halvvejs. Kapacitets-fundamentet er ankommet, og det er <strong>modellen.</strong> En enkelt række på mit ark er en feature, jeg ville have sat to dages håndarbejde af til, leveret for omkring halvtreds dollars. Det var fuldstændig umuligt for to år siden, til enhver pris, med ethvert loop. Loopet ændrede sig ikke; det, der sad inde i det, blev godt nok til, at man kan betro det rigtigt arbejde. Det er container-øjeblikket — den rå kapacitet, der lander.</p>
<p>Det, der ikke er ankommet, er resten af stakken. Der findes ikke et billigt, standardiseret, sikkert sted til rent faktisk at køre autonomi uovervåget — ingen pendant til den managed sky og Kubernetes, der forvandlede &ldquo;jeg har en lille service&rdquo; til &ldquo;og her er det oplagte, overkommelige sted at køre den.&rdquo; Intet siger endnu &ldquo;lad denne agent male løs på egen hånd natten over, uden at det koster en formue eller løber af sporet.&rdquo;</p>
<p>Så: modellen er containerne; Kubernetes er ikke bygget endnu. Og &ldquo;loops&rdquo; — filosofien, størrelseskrigene — er <em>nanoservices</em>-frådet om igen: &ldquo;hvor absurd småt kan vi gøre det?&quot;-overdrivelsen, der udspiller sig på en stak, der stadig mangler sin nederste halvdel.</p>
<h2 id="autonomi-er-gradvist-ikke-en-kontakt">Autonomi er gradvist, ikke en kontakt<a class="gn-heading-anchor" href="#autonomi-er-gradvist-ikke-en-kontakt" aria-label="Link til dette afsnit"><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>Hvilket bringer mig tilbage til, hvor jeg startede. Du vipper ikke fra &ldquo;jeg holder øje med alt&rdquo; til &ldquo;den kører selv.&rdquo; Ingen gik fra én service til to hundrede på en weekend, og ingen ved deres fulde fem går fra overvåget-alt til autonom-alt heller.</p>
<p>Snoren løsnes én verificerbar opgaveklasse ad gangen, begrænset af præcis én ting: hvor checkbar verifikationen er. Det sker allerede. En PR-watcher, der auto-fikser fejlede builds, er snoren allerede løsnet — på en snæver, afgrænset, maskin-checkbar klasse, hvor prisen for at tage fejl er lav, og et grønt check fortæller sandheden. Imens forbliver feature-design og produktionsændringer i snor længe, måske for altid, fordi verifikationen er dømmekraft, og dømmekraft passer ikke ind i et CI-check.</p>
<p>Så når du læser &ldquo;jeg designer loops, ikke prompts,&rdquo; så forstå, hvor det kommer fra. Det er ikke en løgn. Men læg mærke til, hvem der plejer at sige det: folk i de store AI-labs — dem, der bygger modellerne — eller med reelt ubegrænsede token-budgetter, eller begge dele. For dem mangler det manglende fundament ikke; måleren gælder ikke på samme måde, som den gælder for dig. &ldquo;Bare kør loops&rdquo; er en fuldstændig ærlig beskrivelse af <em>deres</em> virkelighed. Det er rapportering fra forkanten: sandt der, hvor det rapporteres fra, med det bedste værktøj og de mest checkbare problemer, solgt som en universel recept, det ikke er endnu — ikke for nogen, der arbejder mod et reelt budget.</p>
<p>Så jeg melder mig ikke ud af debatten — jeg melder mig ud af ordet. Jeg bygger den overvågede kæde, skærper mine egne evner og min verifikation, og lader hver opgaveklasse fortjene sit slæk ét check ad gangen. Det er den del, jeg kan handle på i dag, mens resten bliver diskuteret færdigt.</p>
<p>Hvilket er det samme træk, der viste sig at være rigtigt med microservices: spot frådet, lad størrelseskrigene rase, og bliv ved med at sende den kedelige udgave, der rent faktisk virker.</p>
]]></content:encoded>
      <category>AI</category>
    </item>
    <item>
      <title>En ren maskine</title>
      <link>https://build.golfnext.com/da/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/da/posts/clean-machine/</guid>
      <description>To år efter at en kollegas dotfiles dukkede op i mit GitHub-feed tvang en ny bærbar mig endelig til det. Hvordan jeg genopbyggede min Mac som et repo med chezmoi, kørte mit årlige værktøjs-review som en samtale med Claude, og automatiserede de dele, jeg plejede at klikke mig igennem.</description>
      <content:encoded><![CDATA[<p>I sidste indlæg satte jeg mig et mål for de første halvfems dage: en koldstart på fem minutter. Fra frisk bærbar til kørende kode på under fem minutter — klon projektet, kør det lokalt, ram et live-endpoint, bestå testene, alt sammen før kaffen bliver kold. Den korteste ærlige vej fra en ny medarbejders første morgen til deres første rigtige bidrag. Det er den on-ramp, jeg vil bygge for teamet hos GolfNext, den danske golf-tech-virksomhed, jeg er på vej til at starte hos.</p>
<p>Før jeg kan række det videre til nogen anden, skylder jeg det til min egen maskine. Et nyt job betyder, at en ny MacBook er på vej, så jeg rettede det samme mål mod mig selv først.</p>
<h2 id="det-to-år-gamle-bogmærke">Det to år gamle bogmærke<a class="gn-heading-anchor" href="#det-to-%c3%a5r-gamle-bogm%c3%a6rke" aria-label="Link til dette afsnit"><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 omkring to år siden rullede et <a href="https://github.com/brandstetterm/dotfiles">dotfiles-repo</a> forbi i min GitHub-timeline — det feed, der viser, hvad de folk, man følger, arbejder på. Det tilhørte Manuel Brandstetter, en tidligere kollega og en af de bedste udviklere, jeg har arbejdet sammen med. Han overtog som lead på <a href="https://scrumlr.io">scrumlr.io</a>, det open source-retrospektivværktøj, jeg var med til at bygge, efter at jeg forlod inovex, det tyske konsulenthus, hvor vi havde arbejdet sammen. Så da hans setup rullede forbi, stoppede jeg op og kiggede. Ingen stor pitch, bare der i forbifarten: alt det, der gør en bærbar til <em>hans</em> bærbar, versionsstyret ét sted. Jeg husker, at jeg tænkte, klart og højt: &ldquo;Det burde jeg gøre.&rdquo; Så lod jeg være. I to år.</p>
<p>Hvis du aldrig har holdt dotfiles: det er de små konfigurationsfiler, der stille former din maskine. Din shell-opsætning, dine editor-indstillinger, din git-konfiguration — de dusinvis af små præferencer, du ellers ville bygge op i hånden, fra hukommelsen, hver gang du får en ny computer. Et dotfiles-repo lægger dem under versionsstyring, så maskinen bliver reproducerbar i stedet for håndlavet og glemt.</p>
<p>Grunden til, at jeg aldrig fik gjort det, er den kedelige: det føltes altid som en eftermiddag, jeg kunne bruge på noget mere presserende. Men det smarte ved en helt ny bærbar er, at den fjerner undskyldningen. Du skal sætte maskinen op alligevel. Så kan du lige så godt sætte den op <em>én gang</em>, ordentligt, på en måde du aldrig skal gentage.</p>
<h2 id="det-review-jeg-kører-hver-december">Det review, jeg kører hver december<a class="gn-heading-anchor" href="#det-review-jeg-k%c3%b8rer-hver-december" aria-label="Link til dette afsnit"><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>Det er en vane, jeg har holdt i årevis. Et eller andet sted i den stille uge omkring jul sætter jeg mig ned og gennemgår mine værktøjer. Bruger jeg stadig det bedste til hver opgave, eller bare det, jeg tilfældigvis valgte for tre år siden og aldrig satte spørgsmålstegn ved? Det er som regel en eftermiddag alene — med changelogs, halvt huskede Hacker News-tråde, en smule &ldquo;hvad bruger alle andre nu&rdquo;.</p>
<p>I år havde et forspring. Et par uger forinden var jeg gået på jagt efter noget i stil med nvm, men til <em>alle</em> runtimes, ikke kun Node: Python, Go, Java, det hele. Jeg fandt <a href="https://mise.jdx.dev">mise</a>, som gør præcis det, og at bytte en skuffe fuld af sprog-specifikke versionsstyringsværktøjer ud med ét enkelt var det mest tilfredsstillende, jeg havde gjort ved mit setup i månedsvis. Den lille sejr er nok det, der stille og roligt skubbede et to år gammelt &ldquo;det burde jeg gøre&rdquo; over i &ldquo;gør det nu&rdquo;.</p>
<p>Så i år kørte jeg gennemgangen som en samtale i stedet. Jeg pegede Claude — Anthropics AI-assistent, den samme jeg skal bygge med på arbejdet — mod mit nuværende setup og stillede den skarpe version af spørgsmålet: hvad her er forældet, hvad mangler, hvad ville du ændre, og hvorfor. Så lod jeg den researche og argumentere for sin sag.</p>
<p>Jeg holdt resultatet ærligt ved at gøre det til en diff. Det gamle setup blev på min <code>main</code>-branch; det reviderede gik på en branch, jeg kaldte <code>cleanup</code>. Så hver anbefaling var en linje, jeg kunne læse, acceptere eller afvise, ikke en sort boks, der omskrev min maskine, mens jeg ikke kiggede. Den ramme betød mere, end jeg havde regnet med. Et review, man kan læse linje for linje, er et review, man rent faktisk stoler på.</p>
<p>To ting kom ud af det: en måde at behandle hele maskinen som ét repo, og en kort liste over værktøjer, jeg havde været dum at springe over.</p>
<h2 id="chezmoi-maskinen-som-et-repo">chezmoi: maskinen som et repo<a class="gn-heading-anchor" href="#chezmoi-maskinen-som-et-repo" aria-label="Link til dette afsnit"><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>Værktøjet er <a href="https://chezmoi.io">chezmoi</a>. Det holder dine dotfiles i et git-repo og renderer dem ud på enhver maskine, du ejer — <em>chez moi</em>, passende nok, fransk for &ldquo;hjemme hos mig&rdquo;.</p>
<p>Det, der solgte mig frem for en almindelig mappe fuld af symlinks, er modellen. Filerne i repoet er kilden til sandheden; du <code>apply</code>&lsquo;er dem for at skrive de rigtige filer ind i din home-mappe. Rediger, se diff&rsquo;en, apply, commit. Det understøtter templating (én fil kan tilpasse sig en arbejdsmaskine kontra en privat), og det holder hemmeligheder helt ude af repoet — mine SSH-nøgler bliver inde i <a href="https://developer.1password.com/docs/ssh/">1Password</a>, udleveret til git efter behov, aldrig skrevet til en fil, chezmoi ved et uheld kunne committe.</p>
<p>Den del, der endelig indfriede det to år gamle løfte, er bootstrappet. På en ren Mac kommer hele maskinen tilbage med tre kommandoer:</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>Den sidste linje kloner repoet, installerer mit shell-framework, lægger hver konfigurationsfil på plads og kører et sæt førstegangs-opsætningsscripts. Én navngivningskonvention klarer det meste af magien: en fil ved navn <code>dot_zshrc</code> i repoet bliver til <code>~/.zshrc</code> på maskinen. <code>dot_config/ghostty/config</code> bliver til <code>~/.config/ghostty/config</code>. <code>dot_</code>-præfikset er bare chezmois måde at skrive et indledende punktum uden at forvirre din filbrowser. Når først det falder på plads, læser repoet som et spejl af din home-mappe.</p>
<h2 id="de-værktøjer-gennemgangen-faktisk-ændrede">De værktøjer, gennemgangen faktisk ændrede<a class="gn-heading-anchor" href="#de-v%c3%a6rkt%c3%b8jer-gennemgangen-faktisk-%c3%a6ndrede" aria-label="Link til dette afsnit"><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>Jeg kørte allerede <a href="https://ohmyz.sh">oh-my-zsh</a> med <a href="https://github.com/romkatv/powerlevel10k">powerlevel10k</a>-prompten. Gennemgangen pressede hårdt på for tre tilføjelser, og havde ret i alle tre:</p>
<ul>
<li><strong><a href="https://ghostty.org">Ghostty</a></strong> — en hurtig, moderne terminal. Jeg havde holdt mig til macOS-standarden i årevis af ren træghed. Ghostty er hurtigere, kan temasættes i en almindelig tekstfil (hvilket betyder, chezmoi kan spore den), og renderer de powerline-glyffer, min prompt bruger, uden en separat Nerd Font-installation.</li>
<li><strong><a href="https://github.com/junegunn/fzf">fzf</a></strong> — en fuzzy finder, og den, der ændrede min hverdag mest. <code>Ctrl-R</code> forvandler shell-historik fra en blind pil-op-jagt til en levende fuzzy-søgning. <code>Ctrl-T</code> lægger en filsti ind i det, du er ved at skrive. Pipe hvad som helst ind i den — <code>git branch | fzf</code> — og du får en interaktiv vælger gratis.</li>
<li><strong><a href="https://github.com/ajeetdsouza/zoxide">zoxide</a></strong> — et smartere <code>cd</code>. Den lærer de mapper, du faktisk bruger, og lader dig hoppe efter et fragment af navnet. <code>z dot</code> lander mig i mit dotfiles-repo fra hvor som helst på maskinen, uanset hvor dybt det er begravet.</li>
</ul>
<p>Ingen af dem er eksotiske — det er værktøjer, mange terminal-tunge folk allerede sværger til, og jeg havde bare aldrig sat mig ned for at tilføje dem. At koble dem på er nogle få linjer shell-konfiguration:</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="automatisering-af-de-dele-jeg-plejede-at-klikke-mig-igennem">Automatisering af de dele, jeg plejede at klikke mig igennem<a class="gn-heading-anchor" href="#automatisering-af-de-dele-jeg-plejede-at-klikke-mig-igennem" aria-label="Link til dette afsnit"><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>En frisk Mac er ikke bare dotfiles. Det er en eftermiddag med at downloade apps, trække dem til Programmer, og klikke sig igennem de samme Systemindstillinger, du har sat på hver Mac, du nogensinde har ejet. Det er den del, gennemgangen automatiserede, og den del, jeg er mest stolt af.</p>
<p>Apps kommer fra en enkelt <code>Brewfile</code> — et manifest, <a href="https://brew.sh">Homebrew</a> læser for at installere alt i ét hug:</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>Én fil opremser kommandolinjeværktøjerne, desktop-apps&rsquo;ene (1Password, Firefox, Ghostty, Docker, editoren) og endda Mac App Store-køb. Ny maskine, én kommando, og en snes apps installerer sig selv.</p>
<p>Så er der macOS-indstillingerne. chezmoi kører opsætningsscripts ved første apply, og et af dem er bare en liste af <code>defaults write</code>-kommandoer — den scriptbare side af Systemindstillinger. En smagsprøve:</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>Jeg havde sat hver eneste af dem i hånden på hver Mac i et årti og aldrig tænkt på at skrive dem ned — nu er de en fil. Det er ikke totalt. Apple blokerer bevidst for at scripte de sikkerhedsfølsomme dele, så at give tilgængelighedstilladelser og logge ind på iCloud forbliver manuelt. Men de dusinvis af små knapper, der plejede at æde en eftermiddag, tager nu sekunder, og jeg skal aldrig huske dem igen.</p>
<h2 id="et-godt-review-skærer-lige-så-meget-væk-som-det-tilføjer">Et godt review skærer lige så meget væk, som det tilføjer<a class="gn-heading-anchor" href="#et-godt-review-sk%c3%a6rer-lige-s%c3%a5-meget-v%c3%a6k-som-det-tilf%c3%b8jer" aria-label="Link til dette afsnit"><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>Overraskelsen var, hvor meget samtalen talte mig <em>fra</em>. Et værktøjs-review handler ikke kun om nyt legetøj; det handler om at lægge mærke til det skidt, man har slæbt rundt på.</p>
<p>Så <code>cleanup</code>-branchen slettede ting. De ældre versionsstyringsværktøjer, jeg havde samlet sammen, plus en håndinstalleret Python, blev alle foldet ind i den ene mise, jeg havde taget i brug et par uger forinden. En genvej, jeg havde lavet for at omgå en gammel Docker-finurlighed, der siden er rettet — væk. Et virvar af arbejde-kontra-privat git-konfiguration, jeg havde samlet på tværs af job, fladet ud til én identitet. Hver af dem var en lille beslutning, jeg havde taget for år tilbage af en god grund, og så aldrig genbesøgt, da grunden var udløbet.</p>
<p>Det er den egentlige værdi i at gøre det med en sparringspartner. Overladt til mig selv har jeg en tendens til at tilføje. Bedt om at forsvare hver enkelt del højt, fik jeg endelig fjernet.</p>
<h2 id="hvad-jeg-tager-med-mig">Hvad jeg tager med mig<a class="gn-heading-anchor" href="#hvad-jeg-tager-med-mig" aria-label="Link til dette afsnit"><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>Det imponerende var ikke, at en AI kan skrive en konfigurationsfil — det ved alle, den kan nu. Det var gennemgangen. Den rakte mig ikke et generisk &ldquo;bedste setup&rdquo;; den kiggede på <em>mit</em>, spurgte, hvad jeg faktisk laver hele dagen, og argumenterede for ændringer, der passede, med begrundelsen vedhæftet, så jeg kunne skyde igen, hvor den tog fejl. Mindre et værktøj end det december-review, jeg allerede kører, med en kyndig person på den anden side af bordet.</p>
<p>Og det lukkede en cirkel. Den fem-minutters koldstart, jeg vil give GolfNext-teamet, er præcis det samme, skaleret op: en ny person, en ny bærbar, og en kort vej til rigtigt arbejde uden en uges opsætnings-skat. Jeg byggede bare den personlige version først.</p>
<p>Så hvis du har dit eget &ldquo;det burde jeg gøre&rdquo;-bogmærke, der samler støv — et repo, der engang rullede forbi i din timeline, måske — så er det her dit tegn. Vent på den næste rene maskine. Og sæt den så op én gang. To år for sent slår stadig aldrig.</p>
<h2 id="referencer">Referencer<a class="gn-heading-anchor" href="#referencer" aria-label="Link til dette afsnit"><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>Værktøjerne og dokumentationen, det er værd at bogmærke, nogenlunde i den rækkefølge, de optræder ovenfor:</p>
<ul>
<li><strong><a href="https://chezmoi.io">chezmoi</a></strong> — dotfiles-manager. Start med <a href="https://www.chezmoi.io/quick-start/">quick start</a> og <a href="https://www.chezmoi.io/user-guide/setup/">user guide</a>.</li>
<li><strong><a href="https://brew.sh">Homebrew</a></strong> og <strong><a href="https://github.com/Homebrew/homebrew-bundle">Brewfile / <code>brew bundle</code></a></strong> — installer apps og CLI-værktøjer fra ét manifest.</li>
<li><strong><a href="https://ohmyz.sh">oh-my-zsh</a></strong> — Z shell-framework, med <strong><a href="https://github.com/romkatv/powerlevel10k">powerlevel10k</a></strong>-prompttemaet.</li>
<li><strong><a href="https://ghostty.org">Ghostty</a></strong> — hurtig terminal med konfiguration som tekst.</li>
<li><strong><a href="https://github.com/junegunn/fzf">fzf</a></strong> — fuzzy finder til kommandolinjen.</li>
<li><strong><a href="https://github.com/ajeetdsouza/zoxide">zoxide</a></strong> — et smartere <code>cd</code>, der lærer dine vaner.</li>
<li><strong><a href="https://mise.jdx.dev">mise</a></strong> — én versionsstyring til node, python, go og resten.</li>
<li><strong><a href="https://developer.1password.com/docs/ssh/">1Password SSH-agent</a></strong> — hold private nøgler ude af repoet og væk fra disken.</li>
<li>Repoet, der startede det hele: <strong><a href="https://github.com/brandstetterm/dotfiles">Manuel Brandstetters dotfiles</a></strong>.</li>
</ul>
<h2 id="ps--et-dock-trick-der-intet-har-med-dotfiles-at-gøre">P.S. — et Dock-trick, der intet har med dotfiles at gøre<a class="gn-heading-anchor" href="#ps--et-dock-trick-der-intet-har-med-dotfiles-at-g%c3%b8re" aria-label="Link til dette afsnit"><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>En gratis godbid til de Mac-brugere, der nåede så langt. Det har intet med noget af ovenstående at gøre, men det er den slags lille ting, der stille har glædet mig i årevis: du kan smide blanke mellemrum (spacers) ind i Docken for at dele dine apps op i klynger. Samme <code>defaults write</code>-trick som resten af indlægget. Kør det én gang per spacer, du vil have:</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>Træk i mellemrummene som ethvert andet Dock-ikon, eller træk et ud af Docken for at fjerne det. Bittelille ting. Får en proppet Dock til at føles organiseret.</p>
]]></content:encoded>
      <category>Teknik</category>
      <category>Infrastruktur</category>
    </item>
    <item>
      <title>Et brev til mig selv, før dag ét</title>
      <link>https://build.golfnext.com/da/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/da/posts/letter-day-one/</guid>
      <description>Før min første dag hos GolfNext — og før jeg overhovedet har skrevet under — hvad jeg går ind til, hvorfor jeg forlader et job jeg godt kunne lide, de mål jeg sætter mig for de første halvfems dage, og det kompas jeg tager med.</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>Det er hele beskeden. Den landede på LinkedIn ud af det blå, fra en rekrutterer ved navn Joe — hvilket, i betragtning af hvor ofte den slags kommer ind under et lånt fornavn, måske endda er hans rigtige. Ingen virksomhed, ingen rolle, ingen detaljer. Den slags besked, man normalt arkiverer uden at tænke nærmere over det.</p>
<p>Denne her ender med, at jeg forlader et job, jeg godt kan lide, i en virksomhed jeg ikke har ét ondt ord at sige om, for en virksomhed hvis navn Joe ikke engang ville fortælle mig. Det måtte jeg selv finde ud af. Nå ja — mig selv og en AI.</p>
<p>Jeg skriver det her før min første arbejdsdag. Jeg har ikke skrevet under på kontrakten endnu; jeg kender ikke engang min startdato. At skrive virksomhedens blog, før jeg officielt har jobbet, lyder bagvendt. Måske er det også det. Jeg gør det alligevel, fordi jeg går rundt med flere tanker, end jeg kan rumme, og fordi jeg vil have et nedslag — noget at læse igen om seks måneder og holde op mod det, der rent faktisk skete.</p>
<p>Så det her er to ting på én gang. Historien om, hvordan en besked på to linjer blev til et job, og et brev til en fremtidig udgave af mig selv.</p>
<p>En tilståelse, før vi begynder: Jeg har aldrig skrevet et blogindlæg i mit liv. Jeg begynder nu med vilje — det at skrive fastholder idéer, der ellers bare er damp. Så her er jeg, og tænker højt.</p>
<h2 id="fra-tre-brødkrummer-til-en-virksomhed">Fra tre brødkrummer til en virksomhed<a class="gn-heading-anchor" href="#fra-tre-br%c3%b8dkrummer-til-en-virksomhed" aria-label="Link til dette afsnit"><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>Jeg spurgte Joe, hvad hans besked handlede om. Det viste sig at være en rolle som Lead Engineer i en lille, men veletableret virksomhed: den første hjørnesten i et AI-first-udviklingsteam — et team, der bygger software mest ved at styre AI i stedet for at taste hver linje i hånden. Joe ville ikke nævne virksomheden. I stedet gav han mig brødkrummer — dansk sportstech, eksisteret siden 2012, 400+ anlæg i 18 lande.</p>
<p>Så jeg rakte brødkrummerne til Claude, Anthropics AI-assistent. På sekunder navngav den virksomheden: GolfNext, i Ry, en kort køretur fra hvor jeg bor i Danmark. De bygger det selvbetjeningsudstyr, golfklubber kører på — mere om det senere.</p>
<p>Den lille øvelse er mere eller mindre jobbeskrivelsen. Hvis man kan pege en AI mod tre vage spor og få den til at rekonstruere virksomheden bag dem, så er det jo arbejdet. Så jeg svarede med en fuldt ud AI-genereret besked, åbent mærket:</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>De skrev tilbage. Et par dage senere kørte jeg ud til Ry og tilbragte en time på GolfNext-kontoret, og da jeg gik derfra, vidste jeg, at jeg ville have det. Tegnet var det første, de sagde, da jeg satte mig: <em>&ldquo;Let&rsquo;s just have a chat here and talk openly.&rdquo;</em> Intet teater.</p>
<p>Før jeg bandt mig til noget, ville jeg vide præcis, hvad jeg gik ind til, så jeg spurgte. Thomas Herskind, der står for produktet hos GolfNext, sendte en oversigt over de systemer, de driver i dag. Jeg læste den og skrev tilbage med alt det, jeg mente var værd at tage fat på først, punkt for punkt. Det blev langt. Meget langt.</p>
<p>Thomas&rsquo; svar var på seks ord. <em>&ldquo;Congrats on your first novel, Johann!&rdquo;</em></p>
<p>Jeg grinede højt. Man lærer meget om et sted af, hvordan det driller en, før man overhovedet har skrevet under.</p>
<h2 id="de-70-idéer-før-denne-her">De 70 idéer før denne her<a class="gn-heading-anchor" href="#de-70-id%c3%a9er-f%c3%b8r-denne-her" aria-label="Link til dette afsnit"><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>Intet af det forklarer, hvorfor en besked på to linjer uden detaljer overhovedet fangede min opmærksomhed. Det kræver, at jeg spoler et par måneder tilbage — og først fortæller, hvem der skriver, nu det her er internettet.</p>
<p>Jeg er generalist. En full-stack-udvikler, der aldrig har haft noget imod at lære, hvad et projekt nu krævede. Seksten år i faget: en masse backend, en masse webapps, et par år som teamleder, og medskaber af scrumlr, et open source-værktøj, teams bruger til at holde retrospektiver — de møder, hvor man kigger tilbage på, hvad der gik godt, og hvad der ikke gjorde. Jeg har aldrig været den udvikler, der elsker teknologi for teknologiens skyld. Det, der trækker i mig, er den anden ende: brugeren, designet, værdien, det der gør nogens dag en smule lettere. Og at nå dertil uden alt for mange omveje. Det er CV&rsquo;et på ét afsnit. Kløen er den del, det ikke kan bære.</p>
<p>Jeg havde længe haft en kløe efter at bygge noget af mit eget. Hvad som helst, der måske kunne vokse til en forretning — og en undskyldning for at skubbe Claude, så langt den kunne, og finde ud af, hvor meget rigtig software den egentlig kunne bygge. Det første projekt udsprang af en irritation, jeg havde slæbt med på tværs af job. Release notes — den &ldquo;hvad er ændret&rdquo;-liste, der er ment til dem, der bruger softwaren — bliver ved med at havne i commit-beskeder, de korte noter udviklere efterlader til hinanden. <em>&ldquo;Upgrade ESLint to version 10.&rdquo;</em> Hvem er det til? Så jeg byggede et lille produkt, der lader dig skrive ordentlige ændringsnoter i almindeligt sprog, lige ved siden af koden, og række dem til de produktansvarlige, der burde eje dem. Bygget fra bunden på cirka to uger — det brugerne ser, backenden bag det, pipelinen der sender opdateringer ud af sig selv. Det hele, med AI bag det meste af tastaturet.</p>
<p>Det føltes godt nok til, at jeg blev grådig og rakte ud efter noget meget større: en agent, der holder softwareprojekter i live. Vedligeholder dem, udvider dem, mest på egen hånd, med release-notes-værktøjet som sin første patient. Det var der, jeg ramte en mur, og muren lærte mig mere end de fleste af mine færdige projekter.</p>
<p>Noget af det var omfang. Jeg havde pakket et helt produkt rundt om noget, der burde være startet som et lille eksperiment. Men den dybere lektie handlede om værktøjet. Nutidens AI er virkelig fremragende på en blank side, og til små, velafgrænsede ændringer. Den bliver usikker, i samme øjeblik man beder den rive fat i store stykker kernelogik. Den begynder at hallucinere — selvsikkert at opfinde ting, der ikke er der. Den rydder aldrig helt op efter sig selv. Gammel kontekst hænger ved og lister sig ind i næste forsøg. Et så ambitiøst byggeri kræver en klar plan for <em>hvordan</em> man kommer derhen, før man skriver en eneste linje, og at holde den plan sammen på egen hånd, i stjålne aftener og weekender, var bare ikke noget, jeg kunne magte. Det projekt er parkeret, ikke begravet.</p>
<p>Så jeg skiftede taktik. Jeg satte Claude til at være sparringspartner på mindre ting, den slags jeg kunne gøre færdig på et par uger. Jeg pitchede en idé; den gik ud og undersøgte markedet og kom som regel tilbage med det samme svar — overfyldt, allerede finansieret, lad være. Surt. Også nyttigt.</p>
<p>Efter omkring 70 af dem holdt jeg op med at pitche og stillede det åbenlyse spørgsmål: hvad <em>burde</em> jeg så lave? Svaret var forfriskende ligetil. <em>Hold op med at jage et smart produkt. Find nogen med et rigtigt problem, og hjælp dem med at bygge en softwaredrevet proces rundt om det, der sparer dem alvorligt meget tid.</em> Og da den kender min måde at arbejde på, skitserede den nogenlunde, hvordan det kunne se ud. Et par måneder senere landede Joes to linjer i min indbakke — og rollen bag dem viste sig, da jeg først spurgte, at være et næsten perfekt match til den skitse. Beskeden uden detaljer var præcis det, jeg havde fået besked på at gå ud og finde.</p>
<h2 id="hvad-det-egentlig-handler-om">Hvad det egentlig handler om<a class="gn-heading-anchor" href="#hvad-det-egentlig-handler-om" aria-label="Link til dette afsnit"><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>På høje tid, jeg fortæller, hvad det egentlig var, jeg havde fundet. GolfNext bygger selvbetjeningsværktøjer og hardware til golfklubber — forestil dig en boldautomat, medlemmerne selv betjener på driving rangen, eller et system, der tager den fummelfingrede, fejlbehæftede opgave med at sælge og indløse vouchere og gør den rolig. I årevis har dygtige eksterne partnere bygget det meste af den software, og det har bragt virksomheden langt. Skiftet nu, som de lagde det frem for mig, er, at GolfNext vil have ingeniørvidenen til at bo inde i virksomheden: et team, der forstår hele landskabet indefra, til den slags stabile, langsigtede vækst, der kommer af at eje det, man afhænger af. Ingen dom over dem, der har bygget det indtil nu — bare det naturlige næste skridt for en virksomhed, der regner med at være her længe.</p>
<p>Jeg er en af de første brikker i det. Opgaven: suge landskabet til mig — det samme, som Thomas gav mig et forhåndsblik af i den oversigt — overtage udviklingen bid for bid, og før længe være med til at bygge et team op omkring det.</p>
<p>Her er en tilståelse, der lyder underligt i en ingeniørblog: Jeg har knap skrevet kode i hånden i månedsvis. Kald det et halvt år på mine egne projekter, et par måneder på arbejdet. Ikke fordi jeg holdt op med at bygge. Byggeriet rykkede bare et niveau op. Mere af min dag går nu med at styre AI, gennemgå det, den giver tilbage, og designe de værn, der holder dens output sikkert og korrekt. Det er der, faget er på vej hen — og det er præcis det team, GolfNext beder mig bygge. Ingen kan ærligt fortælle dig, hvor det hele lander; jorden flytter sig med få ugers mellemrum. Men retningen er klar nok til at pege på, og at pege på den er jobbet.</p>
<p>Det er <em>hvad</em>. Før <em>hvordan</em> er der et farvel, jeg skylder.</p>
<h2 id="at-forlade-noget-godt">At forlade noget godt<a class="gn-heading-anchor" href="#at-forlade-noget-godt" aria-label="Link til dette afsnit"><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>Først den ærlige del, ingen nyder at skrive. At sige ja her betyder at forlade Salling Group — en af Danmarks største detailvirksomheder — og jeg vil ikke have et eneste ord af det her læst som en klage. Salling var det perfekte første skridt, da jeg landede i Danmark: en ægte kultur, mennesker der virkelig passer på hinanden, arbejde jeg er stolt af. Jeg leverede ting, der betød noget, under deadlines uden plads til fejl, ved siden af ingeniører, jeg lærte en hel del af. Jeg fik gode venner der. En del af mig er ked af at gå.</p>
<p>Grunden til, at jeg flytter mig, er ikke en fejl ved stedet. Det er en forskel i, hvad jeg er skruet sammen til. I en virksomhed af den størrelse bliver hver beslutning vejet af mange omtænksomme mennesker, hvilket er præcis sådan, en stor detailvirksomhed gør sig fortjent til og fastholder folks tillid. Men det betyder, at det er sjældent at være tæt på rattet, og endnu sjældnere at styre hurtigt. Jeg vil have rattet. Jeg vil pege på en retning og bevæge mig. Den kløe er størstedelen af grunden til, at jeg begyndte at bygge ting ved siden af i første omgang, og det er præcis det, denne nye rolle rækker mig. At forlade noget godt af den rigtige grund er stadig at forlade noget godt. Begge dele er sande på én gang.</p>
<h2 id="de-mål-jeg-sætter-mig">De mål, jeg sætter mig<a class="gn-heading-anchor" href="#de-m%c3%a5l-jeg-s%c3%a6tter-mig" aria-label="Link til dette afsnit"><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>Nu til <em>hvordan</em>. Den mur, jeg ramte på sideprojektet, er hele grunden til, at målene herunder ser ud, som de gør. Hvis AI er stærkest inden for klare grænser, er svaret ikke at prompte hårdere. Det er at bygge et landskab, hvor grænserne er tydelige — hvor kontekst er billig at række videre, hvor en ændring er nem at skære lille, hvor værnene fanger det, modellen tager fejl af, før et menneske skal. For det er der, jeg tror, det hele er på vej hen: den knappe ressource i software bliver, hvor hurtigt hvem som helst — menneske eller AI — kan sætte sig ind i en kontekst, de aldrig har set. Hvis en ny udvikler og en frisk AI-session begge kan forstå en service hurtigt, kan arbejdet gå i gang med det samme, af den der griber det. Hvis de ikke kan, starter hver opgave med arkæologi. Det meste af det følgende handler i virkeligheden om den ene sætning.</p>
<p>Ét forbehold, der betyder noget: Jeg er ikke begyndt endnu. Det her er de standarder, jeg vil holde mig selv til, ikke beslutninger jeg træffer alene — rækkefølgen og prioriteterne bliver til sammen med de mennesker, der har drevet systemerne i årevis.</p>
<p><strong>Gør arbejdet synligt.</strong> Denne blog er mål nummer ét. Når man bygger viden op inde i et team, er det værste, man kan gøre, at låse den inde i private kanaler og enkelte hoveder. At skrive det ned i det åbne tvinger klarhed frem, trækker gode folk til, holder alle ærlige. Så den findes nu. Fremtidige mig: hvis disse indlæg gik i stå, så tag det som et advarselstegn.</p>
<p><strong>Genopbyg forståelsen, ikke bare adgangen.</strong> At overtage fra et eksternt team handler ikke rigtig om at samle adgange og kodeord. Det handler om at genopbygge forståelsen indefra. Så: hver konto tjekket og sat i mit navn, hvert dokument læst og skærpet, indtil jeg kan drive og release hvad som helst i stakken uden at ringe til en ven. Det er den uglamourøse halvdel af de halvfems dage, og det er den halvdel, alt andet venter på.</p>
<p><strong>Byg et landskab, alle kan bygge i.</strong> Det er det, jeg holder mest af. Konkret:</p>
<ul>
<li><strong>En fem-minutters koldstart.</strong> Frisk laptop til kørende kode: forstå projektet, kør det lokalt, ram et live-endpoint, få testsuiten igennem, alt sammen på under fem minutter. Det er rampen ind til at bygge og afprøve, ikke en regelbog for, hvordan man skriver kode. Fremtidige mig, den her kan måles — gå ud og tag tid.</li>
<li><strong>Dokumentation, der bor lige ved siden af koden</strong>, så kontekst er én mappe væk i stedet for én Slack-besked væk.</li>
<li><strong>Automatisering af alt det, vi ellers ville lave i hånden to gange.</strong></li>
<li><strong>Teknisk gæld betalt af, dér hvor den reelt står i vejen</strong> — ikke en hovedrengøring, bare det der spærrer.</li>
<li><strong>Et fælles sæt prompts og presets til AI&rsquo;en</strong>, så alle starter fra den samme gennemprøvede opsætning i stedet for at bygge den op forfra hver gang. Det er det, der gør &ldquo;AI-first&rdquo; til reelt fælles værktøj i stedet for en floskel.</li>
</ul>
<p>Pointen er ikke at være de eneste, der kan bygge. Det er at være kvalitetsporten, der gør en rå idé til noget, man kan stole på, og den hjælpende hånd, der gør det sikkert og let for alle andre at bygge.</p>
<p><strong>Bliv ved tasterne.</strong> Det sagde jeg til Joe, før jeg havde sagt det til nogen hos GolfNext, fordi det ville spare os begge tid. Jeg har ledet et team før, fireogtyve mennesker, og jeg trådte tilbage til at bygge med vilje. <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> Den linje begyndte som et filter for jobbet. Nu er den et mål. Fremtidige mig: hvis du er drevet væk fra koden, ved du, hvad du skal gøre ved det.</p>
<p><strong>Ansæt fremragende folk, og stol så på dem.</strong> Ikke i de første halvfems dage, og ikke min beslutning alene, men det er den vej, det her går, og det former de tidlige valg. Et lille team af fremragende folk har næsten ikke brug for proces. Hold barren høj, også når I er underbemandede. Den del er i det mindste ikke mig, der projicerer min læseliste over på dem: da ansættelser kom på bordet i Ry, fortalte de mig, at de hellere ville køre underbemandet i et par måneder end tage den forkerte person ind bare for at få mere kapacitet. Det fortalte mig mere om stedet, end noget pitch kunne have gjort. Giv folk kontekst i stedet for godkendelser. Bedøm arbejdet, ikke larmen. Sig som udgangspunkt ja, og stop kun for at diskutere ved de døre, man ikke kan gå tilbage igennem. Og lad der være plads — rigtig plads — til at brainstorme, til at skændes om idéer, til at bygge ting, der måske ikke virker. Den kreative, legende tid er ikke en belønning, man tjener ind, når det seriøse arbejde er gjort. I dette fag <em>er</em> den det seriøse arbejde.</p>
<h2 id="et-kompas-ikke-en-regelbog">Et kompas, ikke en regelbog<a class="gn-heading-anchor" href="#et-kompas-ikke-en-regelbog" aria-label="Link til dette afsnit"><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>Målene er planen. Under planen ligger et kompas, og det er værd at vise frem, for det vil forme, hvordan jeg styrer.</p>
<p>Det, jeg ikke møder op med, er et manifest. Jeg tror ikke på, at en kultur kan skrives til live. Den vokser ud af sit miljø og menneskene i det, dag for dag, i tusind små øjeblikke ingen skriver ned — og derfor kan den kun ledes ved eksempel, aldrig ved håndhævelse. Hvad GolfNexts værdier faktisk er, finder vi ud af sammen, i arbejdet. Og jeg mener <em>sammen</em> i begge retninger. Jeg ankommer med overbevisninger — det her indlæg er fuldt af dem — men jeg regner med at blive forandret mindst lige så meget, som jeg forandrer noget, og at tage arbejdsmåder til mig, jeg ikke engang har overvejet endnu.</p>
<p>En del af det skylder jeg inovex, det tyske konsulenthus, hvor jeg tilbragte syv år — den længste strækning af de seksten. De havde en trekant — Medarbejdere, Kunder, Økonomi — og de mente hvert hjørne af den: fair løn, friheden til at vælge de rigtige værktøjer til opgaven, fornuftige og agile arbejdsmåder, ægte omsorg for de mennesker, der laver arbejdet. Det meste af det holder jeg stadig fast i. Min egen vægtning ligger bare en smule anderledes. Hvor deres trekant sætter medarbejdertilfredshed, sætter min <em>Craft</em> — håndværket. Ikke fordi tilfredshed ikke betyder noget, men fordi jeg er kommet til at tro, at den vokser ud af at lave godt arbejde sammen, snarere end at være noget, man kan sigte direkte efter.</p>
<p>Så de tre punkter, jeg styrer efter, er <em>Kunden</em> (sigtet — at løse det problem, folk virkelig ville savne), <em>Craft</em> (svinget — at bygge det godt, og blive bedre ved at bygge det) og <em>Kapital</em> (det lange spil — at holde sig stærk nok til at spille runden færdig, og holde viden inde i huset, så intet vigtigt bor i ét enkelt hoved). Læn dig for hårdt op ad én af dem, og du slicer; få alle tre til at svinge sammen, og bolden falder i.</p>
<h2 id="hvad-jeg-har-læst-og-hvad-jeg-tog-med">Hvad jeg har læst, og hvad jeg tog med<a class="gn-heading-anchor" href="#hvad-jeg-har-l%c3%a6st-og-hvad-jeg-tog-med" aria-label="Link til dette afsnit"><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>En god del af målene ovenfor — den høje bar, tilliden, den næsten fraværende proces — har jeg fra <em>No Rules Rules</em>, Reed Hastings&rsquo; og Erin Meyers bog om, hvordan Netflix bliver drevet. Argumentet, komprimeret: skru talenttætheden op, til du har et team af fremragende folk, brug åbenhjertighed til at holde dem ærlige over for hinanden, og begynd så at fjerne reglerne — for de fleste regler findes for at forhindre dårlige udfald fra middelmådige præstationer, og har man ikke dem, koster reglerne mere, end de sparer.</p>
<p>Den del, jeg vender tilbage til igen og igen, er udgiftspolitikken. Den koger ned til én enkelt instruks: <em>handl i virksomhedens bedste interesse.</em> Det ligner en politik om kvitteringer. Det er det ikke. Det er en erklæring om, hvem der har ansvaret. En regelbog rækker dig et svar, en anden har regnet ud engang, til en situation, der måske ikke længere er din. Én instruks rækker dig <em>målet</em> og lader dig selv finde svaret til den situation, du faktisk står i. Det er mere vægt at bære. Det er også det, der gør dig til en reel del af det hele i stedet for en, der eksekverer andres beslutninger — og det er der, kreativitet og initiativ kommer fra. Man får ikke en uventet god idé ud af en, der følger trin.</p>
<p><strong>Den del, der skal trænes.</strong> Intet af det virker uden åbenhjertighed, og åbenhjertighed kommer ikke gratis — for nogen. Trækket mod at glatte tingene ud er universelt, og at holde af nogen uden at sige tingene er den fejltype, der føles venlig og ikke er det. Fjern reglerne og behold høfligheden, og du får ikke frihed. Du får et team, der stille driver, mens alle gætter.</p>
<p>Så jeg læste også <em>Radical Candor</em>, Kim Scotts bog om præcis det her — brugbar feedback kræver begge halvdele på én gang: man skal faktisk holde af mennesket, og man skal faktisk sige tingene. Deraf dette løfte, på skrift, hvor enhver kan holde mig fast på det: forvent, at jeg er meget klar omkring, hvad jeg mener, hvad jeg har besluttet, og hvorfor — også når svaret er, at jeg tog fejl.</p>
<p><strong>Og alligevel — ikke en kultur, man kan kopiere.</strong> Ét forbehold over det hele. Jeg vil ikke importere Netflix&rsquo; kultur en bloc. Den voksede ud af en bestemt virksomhed, i et bestemt marked, i en bestemt størrelse, og meget af det, der får den til at virke dér, rejser ikke godt. Bogens berømte skarpe kant rejser dårligst af det hele: keeper-testen, hvor enhver, en leder ikke ville kæmpe for at beholde, bliver sendt afsted med en generøs fratrædelse. Den del ville jeg lade blive helt og aldeles. Talenttæthed tjent ved at ansætte omhyggeligt er noget andet end talenttæthed vedligeholdt ved at skære fra — og en så hård kant har meget amerikanske rødder. Det er ikke sådan, jeg ville ønske, et lille team i Danmark skulle føles. Der er mindst lige så meget at hente i, hvordan dygtige folk i virksomheder som Google og Apple har bygget og drevet deres teams — og de kulturer er uenige med Netflix&rsquo; på flere punkter, med vilje. Vores bliver sin egen — formet i Ry, ikke kopieret ind fra Californien. Den virkelige test af alt det her er, hvad alle gør, når ingen håndhæver noget.</p>
<p>Hvilket lander tilbage, hvor udgiftspolitikken startede. Overtag ikke andres regelbog. Overtag vanen med at spørge, hvad der reelt er bedst her, lige nu, med de mennesker vi faktisk har — og bliv ved med at være villig til at ændre svaret, når situationen gør.</p>
<h2 id="en-note-til-fremtidige-mig">En note til fremtidige mig<a class="gn-heading-anchor" href="#en-note-til-fremtidige-mig" aria-label="Link til dette afsnit"><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>Når du læser det her, ved du alt det, jeg ikke gør. Startdatoen. Om de første halvfems dage gik efter planen. Hvilke af disse mål der overlevede mødet med virkeligheden, og hvilke der var sødt naive. Noget af det vil se indlysende ud bagefter. Noget af det vil se sjovt ud.</p>
<p>Jeg ville have det skrevet ned, før jobbet kunne begynde at forme mine svar — mens det hele stadig er overbevisning, før det størkner til erfaring.</p>
<p>Thomas spøgte allerede med, at mit første lange svar til ham læste som en roman. Betragt dette indlæg som efterfølgeren. Længere, og denne gang offentlig. Lad os se, hvor tæt jeg ramte.</p>
]]></content:encoded>
      <category>Kultur</category>
      <category>AI</category>
    </item>
  </channel>
</rss>
