Agenten schreiben Code schneller, als ihn jemand reviewen kann. Hier ist die Ratsche, die verhindert, dass die Codebasis verrottet

Du bittest einen Agenten, ein Modul zu refaktorieren. Er gibt dir einen 800-Zeilen-Diff zurück, du überfliegst ihn, du mergst ihn. Drei Monate später öffnest du diese Datei: 4.000 Zeilen, ein Dutzend Branches pro Funktion, und eine Stunde Lesen, bevor du sicher eine Zeile ändern kannst. Das Problem ist nicht, dass der Agent schlechten Code geschrieben hat — sondern dass die Generierungsgeschwindigkeit die Review-Geschwindigkeit überholt hat. Etwas kann 5.000 Zeilen pro Tag hinzufügen; niemand kann 5.000 Zeilen pro Tag lesen. Codebasen werden selten durch einen einzigen schrecklichen Commit ruiniert; sie werden durch tausend Commits zerdrückt, die jeweils „nur ein bisschen“ hinzugefügt haben.
Am 5. Oktober 2026 hat das Hermes-Repository PR #132646 zusammengeführt, der einen interessanten Mechanismus installiert: Code-Gesundheit kann sich nur verbessern. Er jagt keinem Ideal nach. Er arbeitet wie eine Ratsche — jede Funktion und jede Datei behält ihr eigenes aktuelles Niveau und darf sich nur in eine Richtung bewegen. Hier steht, wie er funktioniert und was es wert ist, übernommen zu werden.
Die Kernregel: getrennte Obergrenzen, kein Borgen
Die Regel ist kurz und hart:
- Was bereits über dem Ziel liegt, darf nicht wachsen. Eine Funktion mit zyklomatischer Komplexität (CC) 40 — eine grobe Zählung unabhängiger Pfade durch den Code — darf bei 40 bleiben oder auf 20 fallen, aber nie 41 erreichen.
- Neuer Code muss das Ziel erfüllen. Neue Funktionen und Dateien müssen erfüllen: CC 20, 300 eigene Zeilen pro Funktion, Verschachtelung 6, 2.000 Zeilen pro Datei.
Die subtilere Hälfte ist, dass du kein Budget borgen kannst. Verstöße gegen Musterregeln (etwa eine verschluckte Exception) werden über einen Fingerprint verfolgt — die Regel, die umgebende Funktion und den Zeilentext. Einen Verstoß zu beheben, bezahlt nie das Hinzufügen eines anderen, und nichts wird über Dateien hinweg gepoolt. Ohne das wird eine Ratsche zu einer Anzeigetafel, die man manipulieren kann.
Keine Baseline-Datei — der clevere Teil
Die meisten Code-Health-Tools wollen eine Baseline: jeden bestehenden Verstoß festhalten, ein Überschreiten verbieten. Diese Datei wird zu einem Konfliktmagneten, weil zwei parallele PRs sie beide bearbeiten wollen und wer zuerst mergt, gewinnt.
Hermes speichert keine Baseline. Es misst im Flug: Es nimmt die Dateien, die eine Änderung berührt, sowohl auf der Basis (erster Parent des Merge-Commits) als auch auf dem Head, und vergleicht. Drei Konsequenzen:
- eine Verbesserung, die auf
mainlandet, wird sofort zur neuen Obergrenze, ohne dass jemand daran denken muss, „die Ratsche anzuziehen“; - parallele PRs geraten nie wegen des Ratschen-Zustands in Konflikt;
- beide Seiten werden immer mit derselben Tool-Version gemessen, sodass der Vergleich aussagekräftig ist.
Verschobener Code behält seine Obergrenze; kopierter Code nicht
Das hält Refactoring lebensfähig:
- Funktionen werden über einen Body-Hash gematcht, der den Namen ignoriert. Eine Funktion, die in eine andere Datei verschoben und leicht bearbeitet wurde (Body-Ähnlichkeit ≥ 0,6, eine ging weg, eine kam an), behält ihre Obergrenze, und wörtlich verschobene Anweisungen auf Modulebene behalten ihre.
- Eine Kopie, die das Original an Ort und Stelle lässt, gilt als neuer Code und muss das Ziel erfüllen.
- Umbenannte Dateien folgen der Rename-Erkennung von git, statt als neu behandelt zu werden.
- Verschachtelte Funktionen sind eigene Einheiten, sodass das Bearbeiten einer Closure nicht der Funktion angelastet wird, die sie enthält. Pythons Komplexität nutzt ruffs C901 minus der direkten verschachtelten Defs.
Was es sonst noch blockiert
Größe ist eine Kategorie. Der vollständige Regelsatz (mit Scope und dem Fix-Text, den der Agent sieht) liegt in scripts/code_health/config.py:
| Kategorie | Regeln |
|---|---|
| Verschluckte Exceptions | S110, BLE001, E722 (blankes except:); ein nur meldendes logger.warning("...: %s", e) zählt weiterhin als verschluckt |
| Korrektheit | F821/F823/F811/F841, RUF006 |
| Kann hängen | ASYNC230/ASYNC240, S113, HX006 (subprocess/urlopen ohne Timeout), HX007 (synchrones Config-I/O in async def), HX008 (get_event_loop), HX009 (gegen Exception geprüfte gather-Ergebnisse) |
| Profil-Scope | HX001 (hartkodiertes ~/.hermes), HX002 (eine neue HERMES_*-Env-Var), HX004 (UnscopedSecretError mit Fallback auf os.getenv), HX005 (Capture zur Importzeit), HX012 (rohes threading.Thread), plus PS-P05/PS-P06 |
| Sonstiges | HX003 (Prozessidentität über argv), HX010 (bool() eines Env-Strings), HX011 (lange if/elif-Leitern) |
Eine Teilmenge ist nur warnend: HX001, HX003, HX010, HX012, PS-P05, PS-P06. Die Stichproben der Reviewer setzen ihre Präzision auf etwa 12–60 %, also drucken sie bei jedem Lauf, blockieren aber nie einen Commit. Eine davon auf blockierend umzustellen, ist eine Ein-Zeilen-Änderung, sobald es Replay-Evidenz gibt.
Die Notausstiegsluke: Ausnahme gewähren, aber begründen
Jede Regel braucht Ausnahmen. Die Syntax ist:
# health: allow BLE001 -- <why>
Der Grund ist erforderlich, und jede in einem Lauf verwendete Allow-Anweisung wird in der Ausgabe aufgelistet, sodass ein Reviewer sieht, wie viele Ausnahmen eine Änderung hinzufügt und was sie sagen. Das ist eine echte Verbesserung gegenüber # noqa, das meist stumm ist.
Ein Befehl, genau was CI ausführt
Der praktischste Teil ist die lokale Erfahrung. python scripts/check führt ruff, die Guard-Checks (footgun / shebang / no-tmp / config-writer / OS-marker / case-collision / lazy-deps / profile-archive) und die Ratsche selbst parallel aus und verwendet das gepinnte ruff von CI — genommen aus der pyproject-dev-Gruppe, mit Fallback auf uvx ruff@<pin>, statt still irgendein ruff zu verwenden, das du gerade hast. Etwa zehn Sekunden über den ganzen Baum.
python scripts/check # alles, gegen die Merge-Base mit origin/main
python scripts/check --only health # nur die Code-Health-Ratsche
python scripts/check --staged # den Index bewerten (was der Pre-Commit-Hook ausführt)
python scripts/check --commit REV # einen Commit bewerten (was der Pre-Push-Hook ausführt)
python scripts/check --install-hook pre-push
python scripts/check --uninstall-hook
Hooks sind Opt-in und mit --no-verify überspringbar; CI nicht. scripts/check braucht Python 3.11+ (für tomllib) und sagt das auch klar, mit Verweis auf die Repo-Virtualenv, statt bei jedem gehookten Commit in einem Traceback zu sterben.
Es gibt auch einen Hauptschalter: ENFORCEMENT = "blocking" | "advisory" | "off" in scripts/code_health/config.py. Entscheidend ist, dass er immer von main gelesen wird — in CI vom ersten Parent des Merge-Commits, lokal von origin/main —, nie von dem getesteten Branch. Ein Ein-Zeilen-Commit auf main dreht die Ratsche für jeden offenen PR herunter, und kein PR kann seine eigene Prüfung lockern.
Es wurde an einem eingefrorenen Replay validiert, bevor es irgendetwas blockierte
Der schlimmste Fehlermodus für ein neues Gate ist, Dinge zu blockieren, die eigentlich in Ordnung sind, also wurde es zuerst gegen die Historie nachgespielt: jeden PR, der zwischen dem 02.10.2026 und dem 03.10.2026 gemergt wurde.
| Prüfung | Ergebnis |
|---|---|
| Eingefrorenes Replay (318 PRs) | 59 würden blockiert. 38 allein durch Dateiwachstum; 21 durch andere Regeln: CC-Wachstum oder eine neue Funktion über CC 20 in 10 PRs, 13 blinde excepts in 9 PRs, fünf F841 in einer Testdatei, ein Test-F811 (ein überschatteter Test, der nie lief), ein S110, eine neue 300+-Zeilen-Funktion |
| Dateigrößen-Ratsche | Der einzige Blocker für 38 von 318 PRs (12 %) in diesem Fenster, meist +1–50 Zeilen in Dateien, die bereits über 2.000 lagen (main.ts, auxiliary_client.py, cron/scheduler.py, die 23k-Zeilen-test_tui_gateway_server.py) |
| Vollbaum-Bericht auf main | Einheiten über Ziel heute: 451 CC, 55 Funktionslänge, 28 Verschachtelung; 119 Dateien; BLE001 6.516, S110 828, HX012 227, HX006 209, HX005 53 — bestehende Schulden, die erst blockieren, wenn sie wachsen |
Die letzte Zeile verdient einen zweiten Blick. Der Mechanismus verlangt nicht, dass jemand die Schulden abbezahlt. Die 6.516 bestehenden verschluckten Exceptions sind eingefroren, nicht eingeplant; sie zählen nur, wenn sie wachsen. Das macht ein solches Gate überlebensfähig — ein sauberes Blatt am ersten Tag zu verlangen, garantiert, dass niemand den Schalter umlegt.
Was du für dein eigenes Repo übernehmen kannst
Wenn dein Team Agenten ebenfalls viel Code schreiben lässt, übertragen sich mehrere dieser Entscheidungen direkt:
- Eine Ratsche, kein Ziel. Nicht „CC muss ab jetzt unter 20 bleiben“, sondern „was bereits darüber liegt, darf nicht schlechter werden, und neuer Code muss die Latte erfüllen“. Die erste Version blockiert jede Änderung an Legacy-Code.
- Im Flug messen, keine Baseline-Datei speichern. Keine Konflikte zwischen parallelen PRs und kein Disziplinproblem, daran zu denken, sie anzuziehen.
- Musterverstöße mit Fingerprint versehen, damit ein Fix nie einen neuen Verstoß erkauft.
- Neue Regeln zuerst als Warnungen ausliefern, ihre Präzision an historischen Replays beweisen und sie dann blockierend machen.
- Für jede Ausnahme eine Begründung verlangen und die Ausnahmen ausdrucken — eine stille Ausnahme ist dasselbe wie keine Regel.
- Den lokalen Befehl identisch zu CI machen, damit niemand raten muss, was die Pipeline sagen wird.
Zum Kontext: Dies ist dasselbe Repository, das ein Agent im September in großem Stil refaktoriert hat: 1.393 Subagenten reduzierten den Nicht-Test-Python-Code um 34,4 %. Das bewies, dass Agenten eine Codebasis in großem Umfang neu schreiben können. Die Ratsche beantwortet die Folgefrage — wie man verhindert, dass sie langsam wieder zurückwächst. Wenn dir wichtig ist, dass Engineering-Constraints in CI statt in einem Wiki landen, sind die Supply-Chain-Prüfungen des Plugin-Katalogs eine weitere Version von „die Regel in die Pipeline schreiben“.
Die Design-Lektion
Die Leute, die das gebaut haben, trafen eine bewusst bescheidene Wahl: nicht das Ideal verfolgen, sondern nur die Linie halten. Die Ziele — CC 20, 300-Zeilen-Funktionen, Verschachtelung 6, 2.000-Zeilen-Dateien — sind kein Tugendstandard; sie sind das Gate, an dem „schlimmer werden“ aufhört. Was es in der Praxis überleben lässt, sind drei Details: bestehende Schulden werden eingefroren statt liquidiert, der Enforcement-Schalter wird immer von main statt vom bewerteten Branch gelesen, und jede Regel muss an historischen Daten beweisen, dass sie keine Fehlalarme produziert. Wenn das Generieren von Code nahezu kostenlos wird, werden Mechanismen wie dieser wertvoller, nicht weniger — weil das Einzige, was teurer wird, die Zeit ist, die ein Mensch zum Lesen braucht.