Den korte version
- AI-kodeagenter er gode til at tilføje kode og dårlige til at opdage, at koden allerede findes. Ladt alene giver de et repo en anden helper for hver første, og enhver læser må tjekke begge for at kunne stole på nogen af dem.
- Løsningen er to vaner skrevet ned, så en agent følger dem. Trådretning: find det nærmeste eksisterende eksempel på samme slags ting og spejl dets form, placering og navne. Hestehale: klatr op ad en stige fra “behøver dette at eksistere” til “det minimum der virker” og stop på det første trin der holder.
- Begge er nu en offentlig evne, MIT-licenseret: grain-skill på GitHub . Den indeholder forfatterreglerne, et review-gennemløb med faste tags og formen på den afsluttende linje der får agenten til at navngive hvad den spejlede.
- I vores hovedrepo siden august: 386 issues og pull requests bærer en plan-kommentar der navngiver det eksempel der skal spejles, på tværs af 1.333 commits ind i et træ på cirka 18.000 filer. 35 steder i koden bærer en
ponytail:-kommentar der navngiver en bevidst forenkling og dens opgraderingsvej. - Det ubehagelige fund: agenten kaldte evnen som en evne nul gange. Reglerne virkede, fordi jeg også lagde dem i projektets CLAUDE.md, i worker-agent-definitionerne og i review-tjeklisten. Læg reglen der hvor agenten allerede læser.
Jeg skrev om dette én gang før, og jeg kunne ikke lide resultatet, fordi det beskrev princippet og ikke mekanismen. Dette er versionen med mekanismen.
Fejlen dette er rettet imod
Tre eksempler fra vores backend, alle virkelige, alle fra agent-skrevne pull requests der bestod deres tests.
En ny REST-route der hentede applikationsservicen direkte fra en factory. Enhver søskende-route i repoet går route til controller til service til repository, hvert hop injiceret. Denne her kollapsede kæden til route til service. Den virkede. Den betød også, at det ene sted hvor request-validering og response-formning lever for den funktion, ikke eksisterede, og den næste agent der rørte funktionen havde to former at vælge imellem.
En overdimensioneret service splittet op i _payloads.py, _protocol.py og _tokens.py, hver et modul med bare async defs, hvor servicen kaldte protocol.discover(...) via import. Fil-længde-tjekket blev grønt. Det som dependency injection findes for at forhindre, kom direkte tilbage: en samarbejdspartner valgt på importtidspunktet, umulig at substituere uden at patche modulet, fraværende i constructoren der dokumenterer hvad servicen afhænger af.
En anden normalizer. Der var allerede én. Agenten fandt den ikke, skrev otte andre linjer der gjorde næsten det samme, og nu hævder to funktioner med forskellig edge-case-adfærd begge at kanonisere det samme felt.
Ingen af disse er en bug i den forstand en test fanger. Hver af dem er en agent der vælger den kode den kunne generere frem for den kode der eksisterede. Gør det på tværs af et par hundrede pull requests, og repoet holder op med at have en form.
Trådretning
Ny kode skal være umulig at skelne fra koden omkring den. Evnen forvandler den sætning til fem trin, som en agent tager før den skriver.
Find eksemplet først. For et endpoint, en service, et repository, en query hook, en store slice eller en komponent: find den nærmeste eksisterende og kopiér dens form, filplacering og navngivning. Opfind ikke struktur når et mønster findes. I vores repo er branding-slicen det navngivne eksempel for backenden, fordi den er lille, ren og spænder over hvert lag, og evnens tabel siger for hver slags artefakt, hvilken fil der skal åbnes og hvad der skal spejles.
Respekter lagene. Præsentation kalder applikation kalder domæne, og infrastruktur implementerer domæne. Et nyt import på tværs af de linjer fejler arkitekturtjekket. Læs lag-dokumentet, udled det ikke igen.
Lokal trådretning mod repoets trådretning. Match dine søskende. Men hvis søskendene selv afviger fra den kanoniske kæde, følg den kanoniske kæde og notér afvigelsen frem for at kopiere driften. “Den matcher de fem andre routes i denne fil” er ikke nok, hvis alle seks springer et lag over som den kanoniske slice bruger. Du sammenlignede for lokalt.
Genbrug den kanoniske helper. Hvis en helper, en type eller et mønster allerede findes i repoet, brug den. To normalizers tvinger enhver læser til at diffe begge.
Navne bærer betydning. Genbrug det ordforråd der allerede er i koden. Et nyt navn for et eksisterende begreb er en bug der venter på en læser.
Testen til sidst er den jeg giver reviewere: hvis du udelukkende ud fra formen kan se hvilke linjer agenten skrev, gik den imod trådretningen.
Hestehale
Doven betyder effektiv, ikke skødesløs. Den bedste kode er den kode der aldrig blev skrevet. Forstå problemet fuldt ud først, hver fil ændringen berører, det virkelige end-to-end-flow, og klatr derefter op ad denne stige og stop på det første trin der holder.
To regler følger med stigen.
En bugfix er en rodårsag, ikke et symptom. En ticket navngiver et symptom. Grep hver caller af den funktion du er ved at røre. Én guard i den delte funktion er en mindre diff end en guard i hver caller, og at patche kun den sti ticketen navngiver efterlader søskende-callerne i stykker.
Hold diffen minimal. Diffen er det review læser. Ingen drive-by-refaktoreringer, ingen omdøbninger, ingen omformatering, ingen “mens jeg er her”-forbedringer, ingen berøring af filer opgaven ikke krævede. Hver af dem begraver den egentlige ændring. Hvis diffen er større end problemet, skær den ned. Forbedringer du har fået øje på, er et opfølgningsissue, ikke ekstra commits på denne branch.
Og grænserne, fordi “doven” er det ord der lettest misforstås. Fjern aldrig ved forenkling input-validering ved en tillidsgrænse, fejlhåndtering der forhindrer datatab, sikkerhed, basale tilgængelighedskrav eller noget brugeren har bedt om. Vær aldrig doven med forståelsen: stigen forkorter løsningen, aldrig læsningen. En lille diff det forkerte sted er en bug mere. Ikke-triviel logik efterlader ét kørbart tjek, det mindste der fejler hvis logikken bryder sammen.
Når agenten med vilje tager et trin og kender loftet, siger den det i koden:
# ponytail: global lock, per-account locks if throughput matters
Der er 35 af dem i vores repo nu. Hver er en beslutning en fremtidig læser ellers ville skulle rekonstruere, og en markør for hvor man skal kigge når loftet rammes.
At få en agent til faktisk at gøre det
Et princip i et dokument er ikke en adfærd. Tre mekanismer gjorde disse to til adfærd, og jeg vil rangere dem i denne rækkefølge.
Briefen bærer eksemplet. I arkitekt-mønsteret skriver hovedmodellen en brief til en worker, og briefens Design-felt navngiver det nærmeste eksisterende mønster der skal spejles, og den minimale rodårsags-ændring. Workeren behøver ikke finde trådretningen. Den får den udleveret. På tværs af vores repo er det hvad de 491 plan-kommentarer på 386 issues og pull requests er: et planlægningstrin der navngiver hvad der skal spejles, før nogen skriver.
Agenten afslutter med hvad den spejlede. Enhver implementers rapport slutter med én linje, mirrored: <path>, eller navngiver det nye mønster og hvorfor ingen søskende passede. Det er billigt, og det er den enkeltstående bedste review-hjælp jeg har tilføjet. En mirrored:-linje der peger på den forkerte slags fil fortæller dig, at diffen er forkert, før du åbner den. En manglende fortæller dig, at agenten ikke kiggede.
Review håndhæver samme barre, i en fast rækkefølge. Vores review-evne har to gennemløb med faste tags, og det er taggene der gør fund sammenlignelige på tværs af reviewere og på tværs af agenter.
Struktur først. Afviger dette fra det nærmeste eksempel, duplikerer det en eksisterende helper, krydser det et lag, ligger det det forkerte sted, rækker det ind i et andet moduls indre, bruger det inkonsistente navne, tager det ejerskab over noget det burde injicere, eller bryder det den kanoniske kaldkæde? Derefter beskæringer. Hvad kunne slettes, erstattes af standardbiblioteket, erstattes af en indbygget funktion, droppes som spekulativt eller skæres ned?
Rækkefølgen er en regel, ikke en præference. Konsistens slår beskæringer. En beskæring der efterlader koden afvigende fra dens søskende er ikke en gevinst, fordi læseren nu må lære to former for at spare otte linjer. Kør struktur først, og beskærings-gennemløbet rører kun kode der allerede matcher.
Hvad der skete i repoet, inklusive den del der ikke virkede
Siden 1. august har vores hovedrepo taget 1.333 commits, de fleste agent-skrevne under review. 386 af issues og pull requests i den periode bærer en plan-kommentar der navngiver det eksempel der skal spejles, 491 sådanne kommentarer i alt. Den kanoniske kæde er standardformen for et nyt endpoint, og de to legacy-routes der springer controlleren over, er listet i dokumentet som en afvigelse under afvikling, ikke som en anden stil.
De 35 ponytail:-kommentarer er et mindre tal end jeg forventede, og jeg synes det er rigtigt. De fleste forenklinger er ikke en kommentar værd. Dem der er, er dem med et loft.
Nu den del jeg helst ikke ville rapportere. Jeg udgav grain og ponytail som en Claude Code-evne: en beskrivelse i evne-indekset, et indhold der indlæses on demand, og en linje i CLAUDE.md der siger, at den skal kaldes før der skrives kode. På tværs af fem ugers transskriptioner kaldte agenten den ved navn nul gange. Den afsluttende mirrored:-linje optrådte i syv svar.
Reglerne landede alligevel. De landede, fordi den samme tekst også stod tre steder hvor agenten læser uden at vælge det: projektets CLAUDE.md, som er i kontekst fra første tur; worker-agent-definitionerne, som siger “mirror the nearest existing example’s shape, location and naming” og “reach for the laziest solution that actually works” som regler, ikke som en evne der skal overvejes; og review-tjeklisten, hvis tags fælder en pull request der ignorerer dem. Evnens indhold blev den kanoniske reference som de tre peger på. Som trigger gjorde den intet.
Så lektionen jeg vil give videre til enhver der pakker konventioner til en agent, er denne. En evne er et godt sted for den lange version af en regel. Den er et dårligt sted for selve reglen, fordi agenten selv skal beslutte at indlæse den, og det gør den for det meste ikke. Læg én-afsnit-versionen i den fil der altid er i kontekst, læg imperativ-versionen i definitionen af den agent der skriver koden, og læg tjekket i det review der gater mergen. Peg så alle tre på evnen for detaljen.
Den offentlige version på github.com/LivinTribunal/grain-skill er skruet sådan sammen: en kort SKILL.md med de regler en agent kan holde i hovedet, det fulde dokument med stigen og anti-mønstrene, review-taggene, og en README der siger hvilket afsnit der skal i din CLAUDE.md, og hvilket der skal i din implementers definition. Erstat det gennemarbejdede eksempel med dit eget kanoniske eksempel, og så er den klaret.

