Teil 2 meiner kleinen Serie endete mit einer Frage, die ich dir schuldig geblieben bin: Eine Spezifikation beschreibt, was gelten soll. Sie erzwingt nichts. Sie ist die Schreibmaschine, nicht das Schloss.
Wer hält das Schloss?
Die ehrliche Antwort ist unbequem. Nicht der Agent, der den Code schreibt. Und auch nicht du, der du müde am Freitagabend auf „Merge“ klickst, weil unten ein grüner Haken steht.
Über dieses Schloss reden wir jetzt. Und ich zeige dir zum ersten Mal etwas, das ich nicht nur aufgeschrieben, sondern gebaut habe.
Der Autor darf seine eigenen Prüfer nicht stellen
Erinnerst du dich an das Fußballspiel aus Teil 1, in dem eine Mannschaft den Schiedsrichter stellt? Niemand, der das Ergebnis will, kann gleichzeitig neutral über das Ergebnis urteilen.
Genau das passiert, wenn derselbe Agent, der den Code schreibt, auch die Tests schreibt, die Gates konfiguriert und am Ende mergt. Er ist Exekutive, Legislative und Judikative in einer Instanz. Das ist keine Entwicklungsumgebung, das ist eine Autokratie. Und die Qualität kollabiert nicht trotz, sondern wegen dieser Machtfülle.
Die Lösung heißt Gewaltenteilung, und sie zieht eine Linie quer durch dein Repo. Auf der einen Seite steht das Produkt: der Code, die Features. Daran darf der Agent frei arbeiten, das ist sein Job. Auf der anderen Seite steht die Verifikation: die Gates, die Spec, die Schwellenwerte, die Regeln, die festlegen, was „bestanden“ überhaupt heißt. Daran darf der Agent nicht einseitig ran.

Zwei Dinge muss dieses Schloss zugleich leisten, und sie ziehen sich durch den ganzen Post: Es muss die Prüfung erzwingen (der Autor darf sie nicht abschalten) und es muss sie an die Wahrheit binden (die Prüfung muss wirklich messen, ob der Code stimmt). Das Erste bringt dieser Abschnitt, das Zweite kommt bei Coverage und Mutation Testing. Fehlt eine der beiden Hälften, ist der grüne Haken wertlos.
Ein Gate, das der Produzent erreichen kann, kann er abschalten
Das ist der Satz, an dem sich alles entscheidet. Eine Prüfung, die der Agent umgehen, lockern oder löschen kann, ist keine Prüfung. Sie ist eine Bitte.
Und Agenten umgehen Prüfungen. Nicht aus Bosheit, das hatten wir in Teil 1: Wenn das Ziel „mach das Gate grün“ lautet, ist „das Gate abschalten“ eine vollkommen rationale Lösung. Wir nennen das Reward Hacking, und es ist messbar, nicht hypothetisch.
Also reicht es nicht, Gates zu haben. Sie müssen für den Produzenten unerreichbar sein. Dafür gibt es vier Hebel, von grob nach fein:
1. Der Ort. Ein Git-Hook auf deinem Laptop (husky und Konsorten) fühlt sich wie ein Gate an, ist aber eins mit Aus-Schalter: git commit --no-verify, und es ist weg. Das echte Gate sitzt serverseitig. Die Gates laufen in der CI, nicht lokal, und Branch Protection erzwingt, dass sie grün sein müssen, bevor irgendetwas in main landet. Der Agent darf einen Branch anlegen und einen PR aufmachen. Mergen darf er nicht. Damit ist --no-verify einfach wirkungslos.
2. Der geschützte Satz. Manche Dateien legen fest, wie geprüft wird: die CI-Konfiguration, die Schwellenwerte, die Spec, die Projektregeln. Wer die ändert, ändert das Gesetz. Also gehören sie unter CODEOWNERS, scharfgestellt über die Branch-Protection-Option „Require review from Code Owners“ (ohne die blockt CODEOWNERS gar nichts, es schlägt nur Reviewer vor). Eine Änderung daran braucht dann ein separates, menschliches Approval. Der Agent kann den Code ändern, so viel er will. Aber die Regel, gegen die der Code geprüft wird, kann er nicht anfassen.
3. Der Wächter der Wächter. Das ist der subtilste. Ein cleverer Produzent schaltet das Gate nicht ab, er durchlöchert es: ein .skip am Test, ein @ts-expect-error über der Zeile, die nicht kompiliert, ein gesenkter Schwellenwert. Alles legitime Werkzeuge, alle missbrauchbar. Dagegen brauchst du einen Wächter, der genau nach diesen Fluchttüren sucht und rot wird, wenn eine neu eingebaut wurde.
4. Append-only-Evidenz. Was einmal bestanden wurde, wird festgeschrieben und ist nicht rückwirkend umschreibbar. Das sind zwei getrennte Mechanismen: Zum einen ist das Ledger eine append-only-Datenstruktur, ein Eintrag wird nie geändert oder gelöscht, eine Korrektur ist immer ein neuer Eintrag. Zum anderen sorgt die Git-Historie für Irreversibilität auf einer anderen Ebene: Eine Force-Push-Sperre auf main verhindert, dass die Historie heimlich glattgebügelt wird. So bleibt der Nachweis, welches Gate wann grün war, ein Protokoll, das man fortschreibt, nicht eines, das man im Nachhinein frisiert. Der Produzent kann ein einmal gefälltes Urteil nicht nachträglich zu einem anderen machen.
Das stumpfste Gate von allen: 100 % Coverage
Ein Gate verdient noch besondere Aufmerksamkeit, weil fast alle ihm blind vertrauen: Test-Coverage.
Coverage misst, welche Zeilen beim Testlauf ausgeführt wurden. Das klingt nach Qualität, ist aber etwas ganz anderes als die Frage, die zählt: Würde ein Bug gefangen? Eine Zeile kann zu 100 % „covered“ sein, ohne dass ein einziger Test prüft, ob ihr Ergebnis stimmt.
Die Rechnung, die mich überzeugt hat, stammt aus einem Beitrag mit dem schönen Titel „Coverage Lies and Mutations Don’t“: Eine Test-Suite mit 100 % Coverage, aber nur 4 % Mutation Score führt jede Zeile aus und verfehlt trotzdem 96 % der möglichen Bugs. Die grüne 100 % fühlt sich großartig an. Sie wäre in dieser Rechnung zu 96 % Theater: Die Tests bestätigen nur, dass Code läuft, nicht dass er stimmt. Und das ist kein akademisches Spielzeug: Selbst Meta setzt Mutation Testing produktiv ein, um genau diese Lücke zu schließen.
Bei agentischem Code ist das doppelt gefährlich, und der Grund hat einen Namen: Test-Code-Kollusion. Wenn dieselbe Instanz den Code und die Tests schreibt, schreibt sie beide mit denselben Fehlannahmen. Der Test bestätigt dann nicht das korrekte Verhalten, er bestätigt den Bug. Beide Seiten sind sich einig, und beide liegen falsch. Genau deshalb diskutiert eine Praktikerin, wie sich klassisches TDD unter Agenten verschiebt, mit dem Befund, dass der Kern erkennbar TDD bleibt, die einzelnen Schritte sich aber verschieben. Mutation Testing ist eine der wenigen Prüfungen, die diese Kollusion von außen aufbrechen: Der injizierte Fehler kommt nicht vom Agenten.
Der Käfig, gebaut
So weit die Theorie. Ich habe lange genug über Käfige geschrieben. Irgendwann musste ich einen bauen.
Er heißt Obol, nach der antiken Münze, und ist ein echtes, lauffähiges Repo, offen auf GitHub. Die Domäne ist bewusst gnadenlos gewählt: ein Credits-Wallet mit einem Ledger, der nur wächst. Eine Buchung wird nie geändert oder gelöscht, eine Korrektur ist immer ein neuer Eintrag. Ein falscher Kontostand ist kein Schönheitsfehler, sondern ein Bug in Geldform, sofort sichtbar.
Dieses append-only-Ledger ist nebenbei das fachliche Spiegelbild des vierten Hebels von eben: Irreversibilität ist hier nicht behauptet, sie ist die Datenstruktur. Der Mensch (ich) baut den Käfig. Der Agent (Claude Code) baut die Features durch den Käfig. Und die echten Treffer landen hier im Blog.
Der Intent ist ein Artefakt, kein Prompt. Das Verhalten steht zuerst als Spec, in der EARS-Notation aus Teil 2 (Easy Approach to Requirements Syntax). So sieht das für ein noch ausstehendes Feature aus, das Belasten des Kontos:
When ein Spend mit ausreichendem Guthaben angefragt wird, shall das System einen ledger_entry schreiben und wallet.entry.recorded emittieren. If derselbe idempotency_key erneut erscheint, shall das System das Originalresultat liefern, ohne einen neuen Eintrag zu schreiben.
Kurz zur Sprachwahl, denn sie ist Teil des Käfigs, nicht Geschmack. Obol ist TypeScript, und der Hauptgrund steht schon im ersten Gate: Der Compiler ist die billigste Prüfung überhaupt, immer an, deterministisch, und fängt eine ganze Fehlerklasse, bevor ein Test auch nur startet. Wenn du Korrektheit nach unten in Gates drückst, ist eine statisch getypte Sprache geschenkte Gate-Fläche.
Der Rest ist Pragmatismus: Agenten beherrschen TypeScript exzellent, das Trainingsmaterial ist riesig und die Tool-Landschaft reif. Mit Effect baust du sauber einen typsicheren Kern, der nicht vertrauenswürdige Eingaben am Rand abfängt, bevor sie in die Logik kommen. Und ich kenne den Stack, mein Team kann ihn lesen und weiterbetreiben. Ein Käfig, den nur einer versteht, hat selbst einen Bus-Faktor von eins.
Drumherum sitzt die Gate-Suite, jede Prüfung ein eigener Pflicht-Job in der CI. Der Typecheck deckt den ganzen Graphen ab, die Tests laufen gegen ein echtes Postgres (via Testcontainers, kein Mock an der DB-Grenze), Stryker ist der assert(true)-Killer. Dazu ein Architektur-Gate gegen Cross-Service-Importe, ein Migrations-Linter gegen destruktive Schema-Änderungen und semgrep als Wächter der Wächter, der neu eingebaute Fluchttüren findet, auch in den Testdateien, die es sonst ignorieren würde.
So sind alle vier Hebel von oben eingelöst. Der Ort (Hebel 1): Jede Prüfung ist ein Pflicht-Job in der CI, Branch Protection erzwingt Grün vor dem Merge, der Agent committet ausschließlich über Branch und PR und mergt nicht. Der geschützte Satz (Hebel 2): Die Verifikations-Dateien stehen unter CODEOWNERS, das Risiko-Tier wird deterministisch aus den berührten Pfaden abgeleitet, nie vom Agenten gewählt. Der Wächter der Wächter (Hebel 3): semgrep sucht nach neu eingebauten Fluchttüren. Und die append-only-Evidenz (Hebel 4): das Ledger als Datenstruktur plus die Force-Push-gesperrte Historie auf main.

„Der Agent merged nicht“ trägt nur, wenn der Agent eine eigene Identität hat: GitHub lässt niemanden den eigenen PR genehmigen. Gewaltenteilung fängt schon bei der Identität an, nicht erst bei den Gates. Wie das ohne einen Account pro Projekt skaliert, kommt in Teil 5.
Klingt wasserdicht. War es nicht.
Der Käfig hatte selbst Bugs
Das ist der Teil, den ich am wenigsten erwartet hatte und der mich am meisten überzeugt hat. Noch bevor der Agent das erste Feature baute, zeigte sich: Der Käfig selbst hatte Lücken. Nur kamen die drei Treffer auf zwei ganz verschiedenen Wegen zutage, und der Unterschied ist die eigentliche Lektion.
Wo ich den Käfig kaputt vorfand
Treffer 1: Der Typecheck prüfte die Tests gar nicht. Strikte Typen über den ganzen Code, dachte ich. Bis ich es prüfte: Die Testdateien lagen außerhalb des Typecheck-Graphen. Ein Test durfte also Typen behaupten, die längst nicht mehr stimmten, ohne dass irgendetwas rot wurde. Die CI stand die ganze Zeit auf Grün, denn das Gate hatte nie hingeschaut. Gefunden habe ich die Lücke nicht, weil eine Pipeline kippte, sondern weil ich die Verifikationsseite durchging, die laut Gewaltenteilung mir gehört, nicht dem Agenten. Der Fix steht als nüchterner Commit in der Historie:
fix(build): typecheck now covers test files (cage gap)
Genau die Stelle, an der Test-Code-Kollusion sich verstecken würde, war ungeschützt, und das grüne Häkchen hatte die ganze Zeit gelogen.
Treffer 2: knip war da, biss aber nicht. knip soll toten Code und ungenutzte Exporte finden, denn unbenutzte Fläche ist genau dort, wo unverkabelter, ungeprüfter Code sich sammelt. Konfiguriert war es. Nur erkannte es die ungenutzten Exporte schlicht nicht, die Konfiguration war daneben. Auch hier gab es kein rotes Gate und keine gescheiterte Pipeline, nur ein Audit, das eine Konfiguration fand, die seit dem ersten Tag nichts geprüft hatte. Ein Gate, das grün leuchtet, ohne je etwas zu prüfen, ist gefährlicher als gar keins, denn es erzeugt Vertrauen, das es nicht verdient. Auch das steht als Commit in der Historie:
fix(ci): knip now detects unused exports and deps (gate was not biting)
Wo der Käfig mich tatsächlich erwischt hat
Treffer 3: Den Migrator-Bug fing nur die Laufzeit. Der Loader, der die SQL-Migration anwendet, hatte einen Fehler in der Verdrahtung. Er lieferte das falsche Zwischenergebnis zurück. Der Compiler sah das nicht, die Typen passten ja. Hier lief das Gate wie vorgesehen: Gefangen hat es der Integrationstest gegen ein echtes Postgres, CI wurde rot, und die Migration war abgelehnt, bevor irgendetwas hätte gemergt werden können. Das ist der einzige der drei Treffer, bei dem der Käfig tatsächlich etwas abgefangen hat, statt beim Nachsehen als leer entlarvt zu werden. Kein statisches Gate der Welt hätte das gesehen. Manche Wahrheit gibt es nur zur Laufzeit, gegen die echte Datenbank.
Drei Lücken, jede eine eigene Lektion, alle noch bevor der Agent die erste Zeile Feature-Code geschrieben hatte. Zwei davon waren stumme Gates, gefunden durch Audit, nicht durch ein rotes Signal, also durch genau die Prüfpflicht, die Gewaltenteilung der Verifikationsseite zuschreibt. Die dritte war ein Gate, das seinen Job tat. Beide Fälle zeigen dasselbe: Der Käfig ist kein Zustand, den man einmal erreicht. Er ist selbst ein Stück Software, das Bugs hat und aktiv geprüft werden muss, sonst bleibt ein kaputtes Gate für immer grün.
Das ist nicht neu. Es ist nur nicht mehr optional.
Die naheliegende Lehre aus diesen Treffern: Ein grünes Gate beweist gar nichts. Du musst jedes einmal rot gesehen haben, bevor du ihm glaubst. Du baust genau den Fehler ein, den das Gate fangen soll, und prüfst, dass die CI kippt.
An dieser Stelle wirst du, wenn du das schon eine Weile machst, mit den Schultern zucken. Geschenkt. Das ist Red-Green aus TDD, „sieh den Test erst scheitern“. Das ist die Grundidee hinter Mutation Testing, die älter ist als die meisten, die sie heute brauchen. Jeder, der mal eine ernsthafte Pipeline gebaut hat, hat einen Build absichtlich kaputtgemacht, nur um zu sehen, ob er kippt.
Stimmt alles. Der Punkt ist nicht, dass das neu wäre. Der Punkt ist, wer das Gate baut und ob er ein Interesse daran hat, dass es greift.
Früher hat derselbe Mensch das Gate gebaut, der auch den Code verantwortet. Schludert er beim Scharfstellen, holt ihn später sein eigener Bug ein. Die „sieh es erst rot werden“-Disziplin war Selbstschutz. Wer sie übersprang, zahlte selbst dafür, und genau deshalb konntest du sie gelegentlich überspringen, ohne dass das System kippt.
Jetzt baut der Agent das Gate. Und sein Belohnungssignal ist grün. Er hat nicht nur kein Eigeninteresse an einem scharfen Gate, er hat das gegenteilige: Aus seiner Sicht ist ein Gate, das nie rot wird, das beste Gate. knip war konfiguriert und blieb trotzdem stumm. Ob aus Versehen oder aus dieser Logik heraus, ist müßig: Niemand, dessen Signal grün ist, ficht ein Gate an, das nichts fängt. Plausibel aussehende, zahnlose Verifikation ist exakt die Sorte Slop, die ein Agent exzellent produziert, nur eine Ebene höher als beim Code.
Was bleibt, ist also die alte Disziplin, jetzt ohne Schlupfloch. Du baust den Käfig ein Gate nach dem anderen. Einbauen, einen passgenauen Verstoß injizieren, rot abwarten, restaurieren, grün abwarten, erst dann das nächste. Der Verstoß muss dabei passen: für knip ein ungenutzter Export, fürs Architektur-Gate ein verbotener Cross-Service-Import, für den Escape-Hatch-Wächter ein .skip im Test. Ein beliebiger Fehler reicht nicht, er testet irgendein Gate, nicht das, das du gerade scharfstellen willst.
Und weil es zur Sache gehört: Ein paar Gates lassen sich jetzt noch gar nicht scharfstellen, weil ihre Zielfläche fehlt. Ein Migrations-Linter ohne Migration prüft nichts, ein Contract-Check ohne Event auch. Die nimmst du in dem Moment ab, in dem das erste Feature ihre Fläche erzeugt. Bis dahin sind sie Gerüst, kein Beweis.
Wenn die Spec dem Code davonläuft, soll es rot werden
Eine Lücke blieb, und sie führt direkt zurück zu Teil 2. Eine EARS-Spec ist schön. Aber was hindert den Code daran, sich still von ihr zu entfernen? Ein Kriterium, das niemand testet, veraltet im Verborgenen, und du vertraust einer Spec, die nicht mehr gilt.
Also bekam Obol ein Gate dagegen. Fertige Werkzeuge dafür gibt es durchaus, OpenFastTrace etwa, nur leben die in der Java-Welt, und für ein TypeScript-Repo wollte ich keine zweite Toolchain nachziehen. Also habe ich es selbst gebaut, im Kern ein paar Dutzend Zeilen Stringvergleich. Die Idee ist ohnehin alt: Requirements Traceability gehört zum Requirements Engineering und ist in sicherheitskritischer Software sogar Pflicht. Neu war hier nur, sie als billiges, stack-natives CI-Gate zu führen.
Jedes Akzeptanzkriterium in der Spec bekommt eine ID, und genau dieselbe ID steht in der Test-Beschreibung:
Spec: [REQ-BAL-02] If das Konto nicht existiert, shall das System 404 liefern. Test: it("[REQ-BAL-02] returns 404 for a missing account", …)
Ein kleiner Scanner gleicht beide Seiten ab. Trägt ein Kriterium keine Test-Referenz, wird die CI rot. Verweist umgekehrt ein Test auf ein Kriterium, das es nicht mehr gibt, ebenso. Eingeführt hat es dieser PR:
feat(tools): spec↔test traceability gate Macht Spec-Drift laut statt still: ein Kriterium, dem der Code davonläuft, hinterlässt einen roten Test, statt unbemerkt zu veralten.
Damit ist der Drift, vor dem ich dich in Teil 2 gewarnt habe, kein stilles Risiko mehr, sondern ein Alarm.
Alle Teile aus dieser Serie
Teil 1 · 11 Min.Warum bessere Prompts deinen KI-Code nicht retten
Teil 2 · 12 Min.Spec-Driven Development, oder: EARS vs. Vibe Coding
Teil 3 · 16 Min. · Du liest geradeMutation Testing bei KI-Agenten: Wer hält das Schloss?
Was du mitnimmst
Der grüne Haken aus dem Titel war eine Behauptung. Jetzt ist er etwas wert, aber nur, weil dahinter Prüfungen stehen, die der Autor des Codes nicht erreichen kann.
Unabhängigkeit ist dabei nur die eine Hälfte. Die andere ist die Bindung an die Wahrheit: dass die Prüfung wirklich misst, ob der Code stimmt, nicht nur, ob er läuft. Genau das war der Unterschied zwischen Coverage und Mutation Testing weiter oben. Ein abschaltbares Gate urteilt nicht unabhängig, ein assert(true) urteilt nicht über die Wahrheit. Fehlt eine der beiden Hälften, ist der Haken grün und trotzdem wertlos.
Und damit du mir nicht zu viel glaubst, die ehrliche Grenze: Das Mutation-Gate prüft bisher nur die reine Kontostand-Projektion, nicht das ganze Repo. Dort steht der Schwellenwert auf 100 %, ein einziger überlebender Mutant kippt die CI. Das ist der Beweis, dass der Regler zupackt, kein vollständiger Schutz. Die geprüfte Fläche wächst mit der Domäne. Ein Käfig wird nicht fertig, er wird gepflegt.
Phase 0 steht: der Käfig und ein Walking Skeleton, ein minimales, aber durchgängig lauffähiges Grundgerüst. Was noch fehlt, ist die spannende Hälfte, der Agent baut die echten Features (Top-up, Spend, die heikle Migration) durch diesen Käfig. Davon erzähle ich, wenn es passiert ist, nicht vorher.
Und weiter?
Du hast jetzt die einzelnen Prüfungen gesehen. Aber Gates sind keine lose Sammlung, sie sind Stationen in einem Ablauf. Und da stellen sich die Fragen, die diesen Post sprengen würden:
Wie greifen die Gates als Loop ineinander? Was passiert eigentlich, wenn eines rot wird, läuft der Agent dann einfach weiter? Und was, wenn nicht einer, sondern fünf Agenten gleichzeitig am selben Repo arbeiten, wer hält dann das Schloss, ohne selbst zum Flaschenhals zu werden?
Darum geht es in Teil 4: So sieht der Loop in der Praxis aus.


Schreibe einen Kommentar