Die Kurzfassung
- KI-Coding-Agenten sind gut darin, Code hinzuzufügen, und schlecht darin zu bemerken, dass der Code bereits existiert. Sich selbst überlassen, geben sie einem Repo einen zweiten Helper für jeden ersten, und jeder Leser muss beide prüfen, um einem von beiden zu vertrauen.
- Die Lösung sind zwei Verhaltensweisen, die aufgeschrieben sind, damit ein Agent sie befolgt. Grain (Maserung): Finde das nächstgelegene existierende Beispiel derselben Art von Sache und spiegle seine Form, seinen Ort und seine Namen. Ponytail: Klettere eine Leiter von „muss das überhaupt existieren“ bis „das Minimum, das funktioniert“ und halte bei der ersten Sprosse an, die trägt.
- Beides ist jetzt ein öffentlicher Skill, MIT-lizenziert: grain-skill auf GitHub . Er enthält die Autorenregeln, einen Review-Durchgang mit festen Tags und die Form der Schlusszeile, die den Agenten benennen lässt, was er gespiegelt hat.
- In unserem Haupt-Repo seit August: 386 Issues und Pull Requests tragen einen Plan-Kommentar, der das zu spiegelnde Exemplar benennt, verteilt über 1.333 Commits in einen Baum von rund 18.000 Dateien. 35 Stellen im Code tragen einen
ponytail:-Kommentar, der eine bewusste Vereinfachung und ihren Upgradepfad benennt. - Der unangenehme Befund: Der Agent hat den Skill null Mal als Skill aufgerufen. Die Regeln funktionierten, weil ich sie zusätzlich in die CLAUDE.md des Projekts, in die Worker-Agent-Definitionen und in die Review-Checkliste geschrieben habe. Leg die Regel dort ab, wo der Agent ohnehin liest.
Ich habe schon einmal darüber geschrieben und das Ergebnis gefiel mir nicht, weil es das Prinzip beschrieb und nicht den Mechanismus. Dies ist die Version mit dem Mechanismus.
Der Fehler, für den das gedacht ist
Drei Beispiele aus unserem Backend, alle echt, alle aus von Agenten geschriebenen Pull Requests, die ihre Tests bestanden haben.
Eine neue REST-Route, die den Application Service direkt aus einer Factory zog. Jede Schwester-Route im Repo geht Route → Controller → Service → Repository, jeder Schritt injiziert. Diese hier faltete die Kette auf Route → Service zusammen. Sie funktionierte. Sie bedeutete aber auch, dass die eine Stelle, an der Request-Validierung und Response-Formung für dieses Feature leben, nicht existierte, und der nächste Agent, der das Feature anfasste, hatte zwei Formen zur Auswahl.
Ein überdimensionierter Service, aufgeteilt in _payloads.py, _protocol.py und _tokens.py, jedes ein Modul aus nackten async defs, wobei der Service protocol.discover(...) per Import aufrief. Die Dateilängen-Prüfung wurde grün. Genau das, wogegen Dependency Injection existiert, kam direkt zurück: ein Collaborator, der zur Importzeit gewählt wird, nicht ersetzbar ohne Patchen des Moduls, nicht vorhanden im Konstruktor, der dokumentiert, wovon der Service abhängt.
Ein zweiter Normalizer. Es gab schon einen. Der Agent fand ihn nicht, schrieb acht weitere Zeilen, die fast dasselbe taten, und jetzt beanspruchen zwei Funktionen mit unterschiedlichem Randfall-Verhalten, dasselbe Feld zu kanonisieren.
Nichts davon ist ein Bug in dem Sinne, wie ein Test ihn fängt. Jedes ist ein Agent, der den Code wählt, den er generieren konnte, statt den Code, der existierte. Mach das über ein paar hundert Pull Requests, und das Repo hat keine Form mehr.
Grain
Neuer Code sollte vom umgebenden Code nicht zu unterscheiden sein. Der Skill verwandelt diesen Satz in fünf Schritte, die ein Agent vor dem Schreiben geht.
Finde zuerst das Exemplar. Für einen Endpoint, Service, ein Repository, einen Query-Hook, eine Store-Slice oder eine Komponente: Lokalisiere das nächstgelegene existierende Exemplar und kopiere seine Form, seinen Dateiort und seine Benennung. Erfinde keine Struktur, wenn ein Muster existiert. In unserem Repo ist die branding-Slice das benannte Exemplar für das Backend, weil sie klein, sauber ist und jede Schicht umfasst, und die Tabelle des Skills sagt für jede Art von Artefakt, welche Datei man öffnen und was man spiegeln soll.
Respektiere die Schichten. Presentation ruft Application ruft Domain, und Infrastructure implementiert Domain. Ein neuer Import über diese Linien hinweg fällt beim Architektur-Check durch. Lies das Schichten-Dokument, leite es nicht neu her.
Lokale Maserung gegen Repo-Maserung. Passe dich deinen Geschwistern an. Aber wenn die Geschwister selbst von der kanonischen Kette abweichen, folge der kanonischen Kette und notiere die Abweichung, statt die Drift zu kopieren. „Es passt zu den anderen fünf Routen in dieser Datei“ reicht nicht, wenn alle sechs eine Schicht überspringen, die die kanonische Slice nutzt. Du hast zu lokal verglichen.
Nutze den kanonischen Helper wieder. Wenn ein Helper, ein Typ oder ein Muster bereits im Repo lebt, nutze es. Zwei Normalizer zwingen jeden Leser, beide zu diffen.
Namen tragen Bedeutung. Verwende das Vokabular wieder, das schon im Code steht. Ein neuer Name für ein bestehendes Konzept ist ein Bug, der auf einen Leser wartet.
Der Test am Ende ist der, den ich Reviewern gebe: Wenn du allein an der Form erkennen kannst, welche Zeilen der Agent geschrieben hat, hat er gegen die Maserung gearbeitet.
Ponytail
Faul bedeutet effizient, nicht nachlässig. Der beste Code ist der Code, der nie geschrieben wurde. Verstehe das Problem zuerst vollständig, jede Datei, die die Änderung berührt, den echten End-to-End-Flow, und klettere dann diese Leiter hinauf und halte bei der ersten Sprosse an, die trägt.
Zwei Regeln reiten mit der Leiter mit.
Ein Bugfix ist eine Grundursache, kein Symptom. Ein Ticket benennt ein Symptom. Grep jeden Aufrufer der Funktion, die du gleich anfasst. Eine Absicherung in der geteilten Funktion ist ein kleinerer Diff als eine Absicherung in jedem Aufrufer, und nur den Pfad zu patchen, den das Ticket benennt, lässt die Geschwister-Aufrufer kaputt zurück.
Halte den Diff minimal. Der Diff ist das, was das Review liest. Keine Drive-by-Refactorings, keine Umbenennungen, keine Neuformatierungen, keine „wenn ich schon mal hier bin“-Verbesserungen, kein Anfassen von Dateien, die die Aufgabe nicht erforderte. Jede davon vergräbt die eigentliche Änderung. Ist der Diff größer als das Problem, kürze ihn zurück. Verbesserungen, die du entdeckt hast, sind ein Folge-Issue, nicht zusätzliche Commits auf diesem Branch.
Und die Grenzen, denn „faul“ ist das Wort, das am ehesten falsch gelesen wird. Vereinfache niemals Eingabevalidierung an einer Vertrauensgrenze weg, niemals Fehlerbehandlung, die Datenverlust verhindert, niemals Sicherheit, Basis-Barrierefreiheit oder irgendetwas, worum der Nutzer gebeten hat. Sei niemals faul beim Verstehen: Die Leiter verkürzt die Lösung, niemals das Lesen. Ein kleiner Diff an der falschen Stelle ist ein zweiter Bug. Nicht-triviale Logik hinterlässt eine ausführbare Prüfung, die kleinste Sache, die fehlschlägt, wenn die Logik bricht.
Wenn der Agent bewusst eine Sprosse nimmt und die Obergrenze kennt, sagt er es im Code:
# ponytail: global lock, per-account locks if throughput matters
Davon gibt es jetzt 35 in unserem Repo. Jeder ist eine Entscheidung, die ein künftiger Leser sonst rekonstruieren müsste, und ein Marker dafür, wo man schauen muss, wenn die Obergrenze erreicht ist.
Einen Agenten dazu bringen, es tatsächlich zu tun
Ein Prinzip in einem Dokument ist kein Verhalten. Drei Mechanismen haben diese zwei zu Verhalten gemacht, und ich würde sie in dieser Reihenfolge ordnen.
Der Auftrag trägt das Exemplar mit. In dem Architekten-Muster schreibt das Hauptmodell einen Auftrag für einen Worker, und das Design-Feld des Auftrags benennt das nächstgelegene existierende Muster zum Spiegeln und die minimale Grundursachen-Änderung. Der Worker muss die Maserung nicht finden. Sie wird ihm in die Hand gegeben. Über unser Repo hinweg ist das, was die 491 Plan-Kommentare auf 386 Issues und Pull Requests sind: ein Planungsschritt, der benennt, was zu spiegeln ist, bevor irgendjemand schreibt.
Der Agent schließt damit, was er gespiegelt hat. Der Bericht jedes Implementierers endet mit einer Zeile, mirrored: <path>, oder benennt das neue Muster und warum kein Geschwister passte. Das ist billig und es ist die beste Review-Hilfe, die ich hinzugefügt habe. Eine mirrored:-Zeile, die auf die falsche Art von Datei zeigt, sagt dir, dass der Diff falsch ist, bevor du ihn öffnest. Eine fehlende sagt dir, dass der Agent nicht geschaut hat.
Das Review erzwingt dieselbe Messlatte, in einer festen Reihenfolge. Unser Review-Skill hat zwei Durchgänge mit festen Tags, und die Tags sind das, was Befunde über Reviewer und über Agenten hinweg vergleichbar macht.
Struktur zuerst. Weicht das vom nächstgelegenen Exemplar ab, dupliziert es einen existierenden Helper, überschreitet es eine Schicht, sitzt es an der falschen Stelle, greift es in die Interna eines anderen Moduls, nutzt es inkonsistente Namen, übernimmt es Besitz von etwas, das es injizieren sollte, oder bricht es die kanonische Aufrufkette? Dann Kürzungen. Was könnte gelöscht, durch die Standardbibliothek ersetzt, durch eine native Funktion ersetzt, als spekulativ gestrichen oder verkleinert werden?
Die Reihenfolge ist eine Regel, keine Vorliebe. Konsistenz schlägt Kürzungen. Eine Kürzung, nach der der Code von seinen Geschwistern abweicht, ist kein Gewinn, denn der Leser muss jetzt zwei Formen lernen, um acht Zeilen zu sparen. Lass Struktur zuerst laufen, und der Cuts-Durchgang berührt nur Code, der schon passt.
Was im Repo passiert ist, einschließlich des Teils, der nicht funktioniert hat
Seit dem 1. August hat unser Haupt-Repo 1.333 Commits aufgenommen, die meisten von Agenten geschrieben und unter Review. 386 der Issues und Pull Requests in diesem Zeitraum tragen einen Plan-Kommentar, der das zu spiegelnde Exemplar benennt, insgesamt 491 solcher Kommentare. Die kanonische Kette ist die Standardform eines neuen Endpoints, und die zwei Legacy-Routen, die den Controller überspringen, sind im Dokument als eine Abweichung aufgeführt, die ausgemustert wird, nicht als zweiter Stil.
Die 35 ponytail:-Kommentare sind eine kleinere Zahl, als ich erwartet hatte, und ich denke, das ist richtig. Die meisten Vereinfachungen sind keinen Kommentar wert. Die es wert sind, sind die mit einer Obergrenze.
Jetzt der Teil, den ich lieber nicht berichten würde. Ich habe Grain und Ponytail als Claude-Code-Skill ausgeliefert: eine Beschreibung im Skill-Index, einen Body, der bei Bedarf lädt, und eine Zeile in der CLAUDE.md, die sagt, ihn vor dem Schreiben von Code aufzurufen. Über fünf Wochen Transkripte hinweg hat der Agent ihn null Mal namentlich aufgerufen. Die mirrored:-Schlusszeile erschien in sieben Antworten.
Die Regeln landeten trotzdem. Sie landeten, weil derselbe Text auch an drei Stellen stand, die der Agent liest, ohne sich dafür zu entscheiden: die CLAUDE.md des Projekts, die ab dem ersten Turn im Kontext ist; die Worker-Agent-Definitionen, die „spiegle Form, Ort und Benennung des nächstgelegenen existierenden Beispiels“ und „greife nach der faulsten Lösung, die tatsächlich funktioniert“ als Regeln sagen, nicht als einen Skill, den man erwägen könnte; und die Review-Checkliste, deren Tags einen Pull Request durchfallen lassen, der sie ignoriert. Der Skill-Body wurde die kanonische Referenz, auf die diese drei zeigen. Als Auslöser tat er nichts.
Die Lektion, die ich jedem weitergeben würde, der Konventionen für einen Agenten paketiert, ist also diese. Ein Skill ist ein guter Ort für die lange Version einer Regel. Er ist ein schlechter Ort für die Regel selbst, weil der Agent sich entscheiden muss, ihn zu laden, und das meistens nicht tun wird. Steck die Ein-Absatz-Version in die Datei, die immer im Kontext ist, steck die imperative Version in die Definition des Agenten, der den Code schreibt, und steck die Prüfung in das Review, das den Merge gated. Dann verweise alle drei für die Details auf den Skill.
Die öffentliche Version unter github.com/LivinTribunal/grain-skill ist so aufgebaut: eine kurze SKILL.md mit den Regeln, die ein Agent im Kopf behalten kann, das vollständige Dokument mit der Leiter und den Anti-Patterns, die Review-Tags und eine README, die sagt, welcher Absatz in deine CLAUDE.md gehört und welcher in die Definition deines Implementierers. Ersetze das durchgearbeitete Beispiel durch dein eigenes kanonisches Exemplar, und es ist fertig.

