Tutorial: Durchgesetzt schlägt versprochen

Serie: ki-code-qualität

Tutorial: Durchgesetzt schlägt versprochen

Teil 10 von 10

Alle Serienteile anzeigen

Tests grün, Pull Request offen, und du liest trotzdem jede Zeile. Bisher hat diese Serie erklärt, woran das liegt. Diesmal wird gebaut: ein System, das die triviale Änderung ohne dich durchwinkt und dir genau die vorlegt, bei denen ein Mensch gebraucht wird. Mit Repo, Konfiguration und den Fehlern aus dem Piloten. Das Referenz-Walkthrough zur Serie „Qualität bei agentischem Code“, das praktische Gegenstück zur Theorie.

Blog » AI » Tutorial: Durchgesetzt schlägt versprochen

Ein verantwortbarer agentischer Merge-Käfig, Schritt für Schritt erklärt

Es ist kurz vor Feierabend. Du hast deinem Agenten ein Feature beschrieben, er hat einen Pull Request aufgemacht, die Tests sind grün. Und jetzt sitzt du da und liest den Diff. Zeile für Zeile. Schon wieder. Weil du ihm nicht traust, und das zu Recht.

Die sieben Teile dieser Serie haben aus verschiedenen Richtungen dasselbe gesagt: Vertrauen ist die falsche Währung. Du baust keinen besseren Agenten, dem du dann glaubst. Du baust ein System um ihn herum, das dir die Frage nach dem Vertrauen abnimmt. Nicht Versprechen, sondern Durchsetzung.

Dieser Post baut dieses System. Am Ende hast du einen Agenten, der einfache Änderungen selbst durch die Pipeline schiebt und merged, ohne dass du hinsiehst, und der dir die Änderungen vorlegt, bei denen ein Mensch gebraucht wird. Nicht weil er brav ist, sondern weil der Käfig ihm gar nichts anderes erlaubt.

Das Warum steht in der Serie. Hier wird gebaut, entlang eines echten Referenz-Repos: Die langen Artefakte, die Skripte, die vollständige ci.yml, die gh api-Aufrufe, liegen dort kopierfertig und verlinkt; dieser Walkthrough geht die Architektur und die Entscheidungen dahinter durch, statt sie abzudrucken. Du wirst GitHub-Konfiguration anfassen, eine CI-Pipeline, eine GitHub App und ein Claude-Code-Plugin. Plan einen halben Tag ein, wenn du dem Referenz-Setup genau folgst; überträgst du die Gates auf deine eigene Domäne oder läufst du in die Stolpersteine aus den Kästen, rechne mit mehr. Am Ende läuft ein Setup, das die drei unteren Risiko-Stufen aus Teil 6 abdeckt: T1 automatisch, T2 mit einem menschlichen Review, T3 mit einem menschlichen Tor und einem Notausgang.

Was wir bauen

Der Käfig hat zwei Hälften, und es lohnt sich, sie von Anfang an auseinanderzuhalten.

Die erste Hälfte ist der Ort: die Regeln, die auf GitHubs Servern leben und die kein Agent von seiner Maschine aus umgehen kann. Required CI-Checks, ein geschützter Satz von Dateien, ein tier-gestuftes Merge-Gate, die Branch Protection, der Auto-Merge. Das ist der eigentliche Käfig. Er ist quellen-agnostisch, ob ein Mensch oder ein Agent den Code geschrieben hat, ist ihm egal, er prüft beide gleich.

Die zweite Hälfte ist der Arbeiter: der Agent, der im Käfig arbeitet. Den orchestriert ein Claude-Code-Plugin namens devloop durch vier Stationen, von der Spezifikation bis zum kritischen Review. Mehr dazu in der zweiten Hälfte des Posts.

Was ist devloop?

devloop ist ein quelloffenes Claude-Code-Plugin von Mayflower (MIT-Lizenz). Es orchestriert den Agenten durch vier voneinander isolierte Stationen, specifyspec-to-testsimplementcritic, getrieben von einem loop-Kommando; init legt den serverseitigen Bindungs-Anker ins Repo, und die getesteten Prüf-Funktionen des Plugins konsumiert der Käfig-Check. Wie die Stationen zusammenspielen, siehst du in der zweiten Hälfte. Kurz gesagt: devloop ist die Schleife, dieser Post baut den Käfig darum herum.

Und devloop ist dabei eine disziplinierte Art, den Käfig zu füttern, nicht die einzig wahre: Der Käfig prüft den Pull Request, nicht seine Herkunft. Ob ihn devloop erzeugt hat, eine andere Schleife, die du dir selbst baust, oder eine Hand, die den Code von Grund auf tippt, ist ihm gleich, er stellt an alle dieselben Gates. Ich nehme devloop, weil es fertig ist und genau das tut. Ein Hinweis noch: Das Plugin ist jung und bewegt sich schnell, Kommandos und Flags können sich ändern. Pinne die Version (der auf eine Version gepinnte Bindungs-Anker weiter unten, precondition-check@v0.8.0, tut genau das), und sieh die Käfig-Hälfte als das Stabile an: Sie ist reine GitHub-Mechanik und hängt weder am Plugin noch an einem bestimmten Produzenten.

Die Reihenfolge ist kein Zufall. Wir bauen zuerst den Käfig, dem du nicht vertrauen musst, und setzen erst dann den Agenten hinein. Ein Käfig, der erst zusammen mit seinem Insassen entsteht, ist keiner.

Was am Ende auf den Servern steht, ist dieses Merge-Modell, die Risiko-Staffel aus Teil 6 in Betriebsform:

Tier / FallTypische ÄnderungMerge, wenn …
T0 / T1Doku, read-only, isolierte Logikalle Gates 🟢 → automatisch
T2normaler Service-Code🟢 + ein menschliches Review (passt das Feature zur Absicht?)
T3Migration, Auth, Contracts, die Gate-Regeln selbst🟢 + ein menschliches Review (darf das so unumkehrbar raus?)
Geschützter Satzdie Verifikation selbstnie ohne bewussten Admin-Override

Die ganze Kunst steckt darin, dass diese Unterscheidung erzwungen wird und nicht auf dem guten Willen des Produzenten beruht.

Ein Wort zur Reichweite: Dieser Post baut die Merge-Autorität pro Stufe, also wer was wann mergen darf. Wie tief eine Stufe darüber hinaus geprüft wird, ist ein eigenes, breiteres Thema: Contract-Tests für Event-Schemata, Migrations-Sicherheit wie Expand-Contract und transaktionale DDL-Tests, Progressive Delivery mit Canary und Auto-Rollback. „T3“ heißt hier „menschliches Tor plus Notausgang“, nicht „das ganze Sicherheitsnetz für Datenmigrationen“. Diese Tiefe hängt stark an deiner Domäne und sprengt diesen Post.

Fangen wir mit der Hälfte an, die das möglich macht.

Schicht 1: Der Agent bekommt eine eigene Identität

Warum. Teil 5 hat es Gewaltenteilung genannt: Der, der produziert, darf nicht der sein, der abnimmt. Auf GitHub ist das nicht bloß ein Prinzip, sondern eine harte Mechanik. Wenn Agent und Mensch sich eine Identität teilen, schlägt GitHubs Selbst-Approval-Regel zu: Du kannst deinen eigenen Pull Request nicht freigeben. Eine Identität heißt also, jeder Merge ist blockiert, und der einzige Ausweg wäre der Admin-Override für alles. Das ist keine Gewaltenteilung, das ist eine offene Hintertür.

Die Lösung ist, dem Agenten einen eigenen GitHub-Principal zu geben, eine GitHub App. Dann ist der Mensch ein anderer Account und kann die PRs des Agenten ganz normal reviewen und mergen. Eine App (statt eines zweiten Benutzerkontos) bringt drei praktische Vorteile mit: Sie skaliert über beliebig viele Repos, sie vergibt kurzlebige Tokens (gültig etwa eine Stunde, kein Langzeit-Token, das in einem Dotfile auf sein Leak wartet), und sie funktioniert auch auf privaten Repos, wo feingranulare PATs nicht ausreichen.

🛠 Bau. Du legst die App unter Settings → Developer settings → GitHub Apps an. Die Permissions hältst du so eng wie die Produzentenrolle es verlangt, und keinen Deut weiter:

  • Contents: Read and write (Branches pushen)
  • Pull requests: Read and write (PRs öffnen)
  • Metadata: Read-only (Pflicht, kommt automatisch)
  • Workflows: bewusst nichts.

Der letzte Punkt ist der wichtigste und der am leichtesten übersehene. Die CI-Definitionen unter .github/workflows/ gehören zum geschützten Satz (Schicht 2). Lokal darf der Agent diese Dateien bearbeiten, aber sein Token bekommt die Änderung nicht ins Repo: Fehlt der App das Workflow-Recht, lehnt GitHub jeden Push ab, der eine Datei unter .github/workflows/ anfasst, ganz egal, was die App sonst darf. Das ist keine Konvention, die man umgehen könnte, sondern eine fehlende Fähigkeit auf dem Push-Pfad. Selbst ein kompromittiertes Token kann die CI-Gates so nicht nach main bringen. Das ist die strengere Wahl, und ich empfehle sie. Vertretbar wäre auch, das Workflow-Recht zu gewähren, aber nur, wenn dein Merge-Gate ohnehin gegen den CI-Token hält und die Workflow-permissions: minimal sind. Vorenthalten ist die Variante, die nicht von dieser Zusatzbedingung abhängt.

Nach dem Anlegen generierst du einen privaten Schlüssel (eine .pem lädt herunter), legst ihn außerhalb des Repos ab und merkst dir App-ID und Installation-ID. Diese drei Dinge, Key, App-ID, Installation-ID, sind alles, was das Token-Minting braucht. Die fertigen Zero-Dependency-Skripte dafür (mint-token.mjs und ein paar Shell-Helfer) stehen kopierfertig in der Identitäts-Doku des Referenz-Repos; ich drucke sie hier nicht ab, weil sie lang sind und dort gepflegt werden.

App-ID und Installation-ID sind keine Geheimnisse, sie sind Identifikatoren und dürfen in Skripten und Doku stehen. Der private Schlüssel dagegen ist der eine Punkt, an dem alles hängt: Wer die .pem hat, kann Tokens für die App ausstellen. Also raus aus dem Repo, chmod 600, nie committen, und bei Verdacht auf der App-Seite widerrufen und neu erzeugen.

Eine Konsequenz fällt sofort an und ist gewollt: Der Bot taucht als eigener Autor auf (…[bot]), und seine PRs stehen auf REVIEW_REQUIRED, bis ein Mensch sie anfasst. Genau das wollten wir.

Schicht 2: Der geschützte Satz

Warum. Der Agent darf produzieren. Was er nicht darf, ist die Regeln ändern, nach denen sein Produkt geprüft wird. Sonst optimiert der gierige Optimierer aus Teil 1 irgendwann nicht die Lösung, sondern das Gate, das ihm im Weg steht. Der geschützte Satz ist die Liste der Dateien, deren Änderung die Verifikation selbst verändert, und genau die hält man dem Produzenten aus der Hand.

🛠 Bau. Zwei Stellen arbeiten zusammen. Die erste ist .github/CODEOWNERS: Jeder Pfad, dessen Änderung die Prüfung berührt, bekommt einen menschlichen Owner. Das sind die CI-Workflows, die Gate-Konfigurationen, das tools/-Verzeichnis mit der Gate-Logik, die Plugin-Konfiguration unter .devloop/ und .specify/, dazu die Manifeste. Der Agent-Bot ist hier nicht eingetragen, und das aus zwei Gründen: Er soll sein eigenes Output nicht abnehmen, und eine GitHub App kann technisch ohnehin nicht als Code-Owner-Reviewer angefragt werden. CODEOWNERS bleibt rein menschlich.

Die zweite Stelle ist eine kleine Datei, die dasselbe noch einmal als Glob-Liste festhält, damit das Merge-Gate (Schicht 5) sie maschinell lesen und Alarm schlagen kann, wenn ein PR den geschützten Satz berührt. Dazu kommt eine Liste der Bot-Logins, damit das Gate weiß, wer nie als menschlicher Reviewer zählt. Beide Dateien liegen unter .devloop/, und beide stehen, du ahnst es, selbst im geschützten Satz.

Der geschützte Satz schützt sich selbst. Eine Liste der unantastbaren Dateien, die der Agent ändern dürfte, wäre witzlos. Deshalb steht der geschützte Satz auf seiner eigenen Liste. Eine Änderung daran ist die heikelste Sorte Änderung, die es im Repo gibt, und läuft entsprechend über das höchste Tier (mehr dazu bei T3).

Damit steht das Fundament: ein Produzent mit eigener Identität und eine klar gezogene Linie um das, was er nicht anrühren darf. Im nächsten Schritt kommt der Boden, auf dem jede Änderung landet, egal von wem.

Schicht 3: Die Gate-Suite, der Boden

Warum. Hier kommt das Leitmotiv aus Teil 3 zum Tragen: Der Boden ist altes Handwerk. Linter, statische Analyse, eine ordentliche Testsuite, Coverage, das gehörte zum sauberen Arbeiten, lange bevor der erste Agent eine Zeile schrieb. Geändert hat sich nicht das Werkzeug, sondern der Spielraum. Was früher die Kür war, die Disziplin, die man unter Termindruck auch mal ausließ, ist beim agentischen Tempo Pflicht, weil der Agent schneller Schrott produziert, als du ihn von Hand auffängst. Der Agent erfindet die Boden-Gates nicht, er nimmt dir nur die Freiheit, ohne sie zu arbeiten.

🛠 Bau. Die Gate-Suite lebt in .github/workflows/ci.yml, und jeder Job darin ist ein Required Check. Wichtig: Der Job-Name ist der Check-Kontext, auf den die Branch Protection später verweist, also benenn sie sprechend. Im Referenz-Repo sind es zehn:

  • typecheck (tsc), lint (Biome), knip (toter Code und ungenutzte Abhängigkeiten)
  • test + coverage (Vitest, gegen echte Container statt Mocks)
  • mutation (Stryker, prüft, ob die Tests überhaupt etwas merken)
  • arch (dependency-cruiser), escape-hatches (Semgrep), migrations (Squawk)
  • tier (die deterministische Tier-Ableitung aus Schicht 4) und trace (Spec-zu-Test-Abdeckung)

Der Punkt ist nicht die genaue Liste, die hängt an deiner Domäne, sondern dass fast nichts davon geschrieben wird. Das sind Werkzeuge von der Stange, die du montierst und konfigurierst, nicht baust. Den Boden stellst du auf, du erfindest ihn nicht. Welche Gates für welche Domäne sinnvoll sind, ist ein Thema für sich und hängt an deinem Stack; die vollständige ci.yml steht im Käfig-Setup des Referenz-Repos.

Eine Unterscheidung, die gleich wichtig wird: Dieser Boden läuft bei jeder Änderung, von der Doku-Zeile bis zur Migration. Er ist billig (Sekunden, nicht Minuten) und deshalb immer an. Gestaffelt wird erst das Teure obendrauf, das zweite Augenpaar, das menschliche Gate. Aber das ist Schicht 4 und 5.

In einer frühen Pipeline-Fassung schluckte ein … | tee tier.json den Exit-Code des eigentlichen Schritts, der Fehler tauchte erst einen Schritt später als verwirrender Folgefehler auf. Lehre: set -o pipefail gehört an den Anfang jedes mehrteiligen CI-Schritts, sonst lügt dir die Pipe ein grünes Gate vor. Und ein generischer Generator für solche Pipelines muss sich ans lokale Format-Gate anpassen (Biome wollte sein JSON einzeilig), sonst stoppt der eigene Linter den ersten Commit. Das Gate hat in beiden Fällen ausgelöst, der Befund war echt.

Schicht 4: Die Risiko-Staffel

Warum. Faustregel 7 aus Teil 6: in der Tiefe alles oder nichts, in der Breite gestaffelt. Du musst nicht jede Zeile mit derselben Wucht prüfen wie eine Datenbank-Migration. Was du in die Hülle legst, darfst du nach Risiko staffeln. Dafür brauchst du eine Staffelung, die jede Änderung danach sortiert, wie schlimm ein Fehler wäre und wie schwer er sich zurücknehmen lässt.

Der entscheidende Trick, und die Stelle, an der die meisten es falsch machen würden: Das Tier wird abgeleitet, nicht gewählt. Es ergibt sich deterministisch aus dem, was die Änderung berührt, nicht aus dem, was der Agent darüber behauptet. Den Grund hast du in Teil 4 gesehen: Lässt du den Produzenten seine eigene Risikoklasse setzen, ist über kurz oder lang alles „trivial“. Wer das Ergebnis grün haben will, redet das Risiko klein. Der Pfad entscheidet, nicht die Meinung über den Pfad.

🛠 Bau. Die ganze Staffel steckt in einer bewusst kurzen Datei, tools/tier-map.json:

{
  "T3": ["services/*/migrations/**", "**/auth/**", "packages/contracts/**", "tools/**"],
  "T2": ["services/**"],
  "T1": ["**"]
}

Lies sie von unten nach oben. Alles ist erst mal T1, der billige Boden. Normaler Service-Code ist T2 und bekommt ein menschliches Review obendrauf. Und vier Dinge sind T3: Migrationen, alles unter auth, die Event-Contracts zwischen den Services und, die eigentliche Pointe, tools/ selbst, also die Gate-Logik. Wer die Wächter ändert, löst das höchste Tier aus. Der Käfig stuft seine eigene Bauanleitung als das Riskanteste ein, was es gibt.

Diese vier Pfade sind die Wahl dieses Projekts, kein Kanon. Deine riskanteste Fläche sieht vielleicht ganz anders aus: Zahlungslogik, personenbezogene Daten, Infrastruktur-als-Code, öffentliche API-Schemata, die Konfiguration deiner Feature-Flags. Was bei dir teuer oder unumkehrbar ist, gehört hier nach T3, und die Datei bleibt trotzdem so kurz. Du beschreibst hier dein Risiko, nicht meins, das ist der ganze Sinn einer abgeleiteten Staffel: Sie macht deine Risiko-Einschätzung explizit und versionierbar, statt sie im Kopf des Reviewers zu lassen.

Wenn du hier einen Pfad nach T2 oder T3 hebst, musst du ihn auch in CODEOWNERS (Schicht 2) eintragen. Sonst verlangt die Staffel zwar ein menschliches Review für diesen Pfad, aber GitHub kennt keinen Owner, der es geben müsste, und die Änderung mergt still ungeprüft, genau dort, wo du es am wenigsten willst. Im Piloten wurde aus dieser Kopplung eine Invariante: Die T2- und T3-Pfade der tier-map müssen eine Teilmenge der CODEOWNER-Pfade sein. Und Invarianten, auf die man sich verlässt, gehören bewacht, nicht gemerkt. devloop bringt dafür einen Drift-Wächter mit (check-codeowners), der als CI-Step fehlschlägt, sobald ein T2- oder T3-Pfad keinen Owner hat. Du bewachst die Invariante also nicht im Kopf, sie kommt mit dem Plugin. Eine ungeschützte Voraussetzung wird so selbst zum Gate.

Zwei Dinge gibst du dabei besser offen zu. Erstens ist der Pfad ein Stellvertreter fürs Risiko, kein Maß davon: Eine Doku-Datei, die zufällig unter auth/ liegt, landet zu hoch; ein gefährlicher Default mitten im Service-Code zu niedrig. Deshalb kalibrierst du die Ableitung bewusst konservativ, lieber zu viel Reibung als ein Loch, und ein Mensch darf eine Änderung hochstufen, nie heruntersetzen. Der Pfad ist dabei nur die einfachste Form der Ableitung. Die stärkere verankert das Tier an der Wirkung statt am Ort: was eine Migration tatsächlich anfasst, ob ein Diff eine veröffentlichte API bricht, ob eine Änderung über den Aufruf-Graphen einen kritischen Pfad erreicht. Das fängt genau die Fehlstellen des Pfad-Stellvertreters und ist die Ausbaustufe, sobald die Glob-Liste an ihre Grenze stößt. Obol fährt bewusst die Pfad-Variante, weil sie ohne Zusatzwerkzeug auskommt und für ein Repo dieser Größe genügt. Zweitens, und das ist die teuerste Lektion aus dem Piloten:

Es reicht nicht, das Tier abzuleiten, ein Gate muss es auch nutzen. Im Piloten verlangte ein Merge-Gate zwischenzeitlich für jeden PR ein menschliches Approval, egal welche Klasse. Klingt sicher, war ein Eigentor: T1 verlor seinen automatischen Merge, und die Tempo-Hälfte der ganzen Idee fiel weg. Diese Tier-Blindheit tauchte im Piloten gleich mehrmals an verschiedenen Stellen wieder auf, jedes Mal mit demselben Effekt. Lehre: Das Tier muss eine Wahrheit sein, deterministisch aus dem Pfad abgeleitet, und jede Stelle, die staffelt, muss diese eine Wahrheit konsumieren statt sie nachzubauen. Zwei Ableitungen driften früher oder später auseinander.

Damit hast du eine eindeutige Risikoklasse pro Änderung. Was noch fehlt, ist die Stelle, die aus dieser Klasse eine Merge-Entscheidung macht.

Schicht 5: Der Bindungs-Anker

Warum. Genau diese Stelle ist der Bindungs-Anker, ein eigener Required Check, den der Agent nicht umgehen kann. Aber hier sitzt die kniffligste Stelle des ganzen Käfigs, und sie ist kontraintuitiv: Der Anker ist nicht das Approval-Tor. Das wäre eine Falle, in die wir erst hineingelaufen sind (gleich der Stolperstein). Er fängt das, was GitHubs eingebaute Regeln nicht können.

🛠 Bau. Der Workflow devloop-precondition-check.yml kommt mit /devloop:init ins Repo (dazu später), und seit Version 0.7.0 ist er angenehm dünn: Er referenziert eine öffentliche composite action des Plugins, statt deren Logik lokal nachzubauen.

- uses: actions/checkout@v4
  with:
    ref: ${{ github.event.pull_request.head.sha }}
    fetch-depth: 0
- uses: mayflower/devloop/.github/actions/precondition-check@v0.8.0
  with:
    github-token: ${{ github.token }}

Das ist die ganze Verdrahtung. Kein npm-Paket, kein Vendoring, kein Cross-Org-Token: GitHub checkt das öffentliche devloop-Repo (mit mitgeliefertem dist/) in den Runner, also läuft der Anker in jeder Org. Innerhalb der Action laufen vier Prüfungen:

  • derive-tier leitet das autoritative Tier aus dem Diff ab, aus dem Pfad, nicht aus einer Behauptung des Agenten.
  • check-codeowners, der Drift-Wächter aus Schicht 4: jeder T2/T3-Pfad muss einen Owner haben.
  • verify-review prüft fail-closed auf zwei Dinge: ob der geschützte Satz berührt wurde (Tampering) und ob ein vorhandenes Approval gültig ist (nicht vom Bot, nicht vom Autor selbst, nicht veraltet).
  • verify-unskip sichert die Naht zwischen Spec und Implementierung (dazu beim T2-Lauf mehr).

Der Anker läuft auf zwei Events: wenn ein PR aufgeht und wenn ein Mensch ein Review abgibt. Das zweite ist nötig, damit das Approve den Check neu auslöst.

In einer früheren Fassung war der Anker selbst das Tor: Er failte, solange ein T2/T3-PR kein Approval hatte. Klingt logisch, hatte aber einen fiesen Nebeneffekt. Weil der Check auf zwei Events läuft, blieb der Vor-Approve-Lauf als rotes FAILURE am Commit kleben, während der Lauf nach dem Approve grün war. GitHub sah das alte Rot und blockierte den Merge. Die Lehre, und der Grund für das Design oben: Das Approval-Tor gehört dorthin, wo GitHub es nativ und zustandslos kennt, in die Branch Protection (Schicht 6). Der Check passt, solange ein PR nur wartet, und failt nur bei einem echten Problem. So vergiftet kein veralteter Lauf den Merge.

/devloop:init pinnt die Version der Action; hochziehen geht per /devloop:init --force. Der vollständige Workflow steht im Käfig-Setup des Referenz-Repos.

Schicht 6: Branch Protection und Auto-Merge

Warum. Hier wird aus Konfiguration Autorität. Alles bisher Gebaute zählt nur, wenn es auf GitHubs Servern erzwungen wird und nicht auf der Maschine des Agenten. Ein Gate, das der Produzent erreichen kann, kann er abschalten.

🛠 Bau. Zwei gh api-Aufrufe und ein kleiner Workflow. Zuerst die Branch Protection auf main. Die Required-Kontexte sind die Job-Namen aus Schicht 3 plus der Bindungs-Anker. Und dann das Stück, das die ganze Tier-Staffelung trägt, das Review-Objekt:

"required_pull_request_reviews": {
  "require_code_owner_reviews": true,
  "required_approving_review_count": 0,
  "dismiss_stale_reviews": true
}

Lies die zwei Zeilen zusammen. required_approving_review_count: 0 heißt: Im Normalfall braucht ein PR null Approvals. require_code_owner_reviews: true heißt: Wenn ein PR aber einen Pfad berührt, der in CODEOWNERS einen Owner hat, dann braucht er dessen Review. Das ist der ganze Trick. Ein T1-PR berührt keinen Owner-Pfad und merged sich allein; ein T2/T3-PR berührt einen und wartet auf einen Menschen. Die Tier-Staffelung fällt aus der Pfad-Sensitivität von CODEOWNERS heraus, ohne dass die Branch Protection das Wort „Tier“ je gehört hätte. dismiss_stale_reviews sorgt dafür, dass ein neuer Push ein altes Approval verwirft, freigegeben wird immer der aktuelle Stand.

Dazu zwei bewusste Schalter: enforce_admins: false lässt dem menschlichen Owner einen Notfall-Override (den brauchst du gleich beim Käfig-Bau selbst), und required_linear_history: true erlaubt nur Squash- oder Rebase-Merges. Die vollständigen gh api-Blöcke stehen in der Branch-Protection-Doku.

Zuletzt der Vollzug: Ein kleiner Workflow auto-merge.yml schaltet für Bot-PRs GitHubs natives Auto-Merge scharf. Der Merge passiert dann serverseitig, sobald alle Required Checks grün sind und das eventuell nötige Owner-Review vorliegt. Dieser Workflow muss das Tier nicht kennen, die Branch Protection entscheidet ja schon, ob ein Review fehlt. Serverseitig heißt auch: unabhängig davon, ob die Agenten-Session noch läuft.

strict: true verlangt, dass ein PR auf dem aktuellen Stand von main ist, bevor er merged. Laufen zwei Bot-PRs gleichzeitig durch, merged der schnellere zuerst und rückt main vor; der zweite fällt auf „behind“ und bleibt hängen. GitHubs Auto-Merge zieht eine zurückgefallene Branch nicht von selbst nach, das braucht einen update-branch-Anstoß. Gewollt ist die Serialisierung (kein Merge auf veraltetem Stand); den Anstoß muss aber jemand geben. Ein Agent, der seinen PR beobachtet, stößt das update-branch im BEHIND-Fall selbst an (per REST), danach läuft die CI erneut und der scharfe Auto-Merge vollzieht den Merge.

Der Haken: Das geht nur, solange die Agenten-Session noch lebt. Dauert das Warten länger, ist sie meist längst beendet, und dann stößt niemand mehr nach. Und GitHub nimmt dir das Nachziehen nicht ab: Die Repo-Option „Always suggest updating pull request branches“ zeigt nur einen manuellen Button, sie aktualisiert nichts von selbst. Wer paralleles Mergen wirklich unbeaufsichtigt will, braucht also entweder einen kleinen Workflow, der zurückgefallene PRs nachzieht, oder GitHubs Merge Queue, die jeden Eintrag serverseitig gegen den aktuellen Stand neu testet.

Damit steht der Käfig. Bevor ein Agent hineindarf, schauen wir ihn uns von innen an.

Die drei Stufen am lebenden Käfig

Drei Stufen, und der Knackpunkt ist eine Unterscheidung, die ich anfangs selbst nicht sauber hinbekam: T2 und T3 sehen gleich aus, beide holen einen Menschen, aber sie fragen ihn etwas Verschiedenes.

  1. T1, der Agent allein. Eine Doku-Zeile, ein read-only-Helfer, isolierte Logik. Kein Owner-Pfad, also kein Review. Alle Gates grün, Auto-Merge, fertig. Du erfährst davon, wenn du willst, nicht weil du musst.
  2. T2, der Mensch fragt nach der Absicht. Normaler Service-Code. Hier holt der Käfig ein menschliches Review, und die Frage dahinter ist: Tut das Feature, was wir gemeint haben? Das ist genau die Frage, die kein Gate beantworten kann, der Intent-Abgrund aus Teil 2 und 7. Ein grüner Test sagt „der Code erfüllt die Spec“, er sagt nichts darüber, ob die Spec das Richtige wollte. Und diese Frage beantwortest du oft am besten, indem du das Gebaute anfasst: Der PR könnte eine Preview- oder Testumgebung mitbringen, du oder der PO klickt das Feature durch und prüft es gegen das, was im Kopf war. Das Review ist hier nicht Formsache, es ist die Stelle, an der ein Mensch den Intent verbürgt.
  3. T3, der Mensch verantwortet die Unumkehrbarkeit. Migrationen, Auth, die Event-Contracts, die Gate-Logik selbst. Dasselbe Review-Tor wie bei T2, aber die Frage ist eine andere: Darf das so raus, obwohl es sich nicht mehr zurücknehmen lässt? Hier geht es nicht um „passt’s“, sondern um Vorsorge gegen einen Schaden, dessen Tail ruinös ist (Teil 6). Und der geschützte Satz, die Verifikation selbst, geht nie ohne einen bewussten Admin-Override durch. Das ist kein Schlupfloch, das ist der vorgesehene Weg für die heikelste Sorte Änderung.

Das Schöne daran: Du musst dir kein künstliches T3-Beispiel ausdenken, der Käfig liefert es beim eigenen Bau. Jede Änderung an den Gates, an CODEOWNERS, an der tier-map liegt unter tools/ oder im geschützten Satz, ist also T3 und musste durch genau dieses Tor. Der Käfig stuft seinen eigenen Bau als das Riskanteste ein, was es gibt, und zwingt dich, ihn von Hand abzunehmen.

Der allererste PR, der den Käfig verdrahtet, berührt den geschützten Satz, bevor es überhaupt einen Käfig gibt, der ihn freigeben könnte. Er failt fail-closed. Genau dafür ist der Admin-Override da: Der Mensch nimmt den Bau bewusst ab. Dazu kommt eine zweite Grenze, die du in Schicht 1 gelegt hast: Die Agenten-App hat kein Workflow-Recht, kann die CI-Dateien also gar nicht pushen. Den Käfig-Bau pusht der Mensch, der Agent kann es nicht, selbst wenn er wollte.

Steht der Käfig? Der Wegwerf-PR

Bevor ein Agent einzieht, ein Beweis statt eines guten Gefühls. Zwei Schritte.

Erstens prüft devloop check-guardians ., ob alle Wächter verdrahtet sind: der Bindungs-Anker, der geschützte Satz, die Identität. Exit 0 heißt: steht.

Zweitens der ehrliche Test. Push als Bot einen trivialen T1-PR, reine Doku. Wenn der Käfig steht, läuft er durch alle Gates und merged sich serverseitig selbst, ohne dass du ihn anfasst. In der Merge-Info steht dann nicht dein Name, sondern app/github-actions. Das ist der Moment, in dem du dem Käfig zum ersten Mal beim Arbeiten zusiehst, mit einer Wegwerf-Änderung, bei der nichts passieren kann.

Vertiefung: Wie der Käfig sich gegen Drift wehrt

(Wenn du erst einmal nur nachbauen willst, überspring diesen Abschnitt, der Agent zieht gleich ein. Wenn dich interessiert, warum ein Käfig nicht nach drei Monaten verrottet, lies weiter.)

Ein Käfig wird nicht einmal gebaut und steht dann für immer. Er erodiert. Code-Basen driften, langsam, unauffällig, immer in Richtung „funktioniert gerade noch“. Das Tückische ist, dass keine einzelne Änderung den Verfall verursacht, sondern die Summe vieler kleiner, die je für sich harmlos aussahen. Genau dagegen braucht der Käfig nicht ein Gate, sondern für jede Art von Drift einen eigenen Wächter. In Obol sind es fünf.

1. Strukturelle Drift: der Modulgraph verheddert sich. Die häufigste Erosion: Ein Service greift „nur dieses eine Mal“ direkt in den Code eines anderen, ein Import schließt einen Kreis, eine Datei bleibt als toter Code zurück. Nach einem halben Jahr ist aus sauberen Service-Grenzen ein Knäuel geworden, das niemand mehr entwirren will. Der Wächter ist dependency-cruiser, und seine Regeln sind in Obol bewusst knapp:

  • Kein Service importiert den Quellcode eines anderen. Geteilt wird ausschließlich über packages/contracts. Das ist die Service-Grenze, nicht als Bitte im Wiki, sondern als Regel, die den PR rot macht.
  • Keine Zyklen. Ein zirkulärer Import macht den Modulgraphen nicht mehr durchschaubar.
  • Keine Orphans. Verwaiste Module sind meist toter Code (mit dokumentierten Ausnahmen für Laufzeit-Einstiegspunkte, die gestartet und nicht importiert werden).

Der Witz an einem solchen Gate: Es macht die Architektur-Entscheidung explizit und versionierbar. „Services teilen nur über Contracts“ steht nicht in einem Confluence-Artikel, den keiner liest, sondern in einer Datei, die jeden Verstoß sofort meldet.

2. Escape-Hatch-Drift: die Schlupflöcher häufen sich. Der Agent ist der gierige Optimierer aus Teil 1, und seine Belohnung ist grün. Findet er eine Abkürzung, nimmt er sie: ein as any, das den Typecheck beruhigt, ein .skip, das einen lästigen Test stilllegt, ein disable-Kommentar, der den Linter zum Schweigen bringt. Jede für sich erklärbar, in Summe ein Käfig voller Löcher. Der Wächter ist ein semgrep-Regelsatz (tools/semgrep-escape-hatches.yml), der genau diese Fluchttüren sucht und den PR stoppt, bevor sie sich einnisten. Dasselbe Prinzip wie das .skip-Verbot aus Teil 3, eine Etage breiter: Die Wege, ein Gate zu umgehen, sind selbst ein Gate wert.

3. Spec-Drift: Code und Absicht laufen auseinander. Eine Spezifikation wird geschrieben, der Code entsteht, und dann lebt der Code weiter, während die Spec einschläft. Nach einer Weile beschreibt sie ein Programm, das es nicht mehr gibt. Der Wächter ist trace-coverage (tools/trace-coverage.ts): Er hält die Verbindung zwischen Anforderungen und Tests nach und macht laut, wenn etwas ohne Spur driftet. Der Spec-Intent-Faden aus Teil 2, in ein Gate gegossen: nicht „gibt es eine Spec“, sondern „deckt sich, was läuft, noch mit dem, was beschrieben wurde“.

4. Invarianten-Drift: die Voraussetzung eines Wächters bröckelt. Jetzt wird es rekursiv, und das ist die feinste der fünf. Die Wächter selbst verlassen sich auf Voraussetzungen, und auch die können driften. Der precondition-check aus Schicht 5 erzwingt für T2/T3 ein Owner-Review, aber nur, wenn jeder T2/T3-Pfad in CODEOWNERS abgedeckt ist. Hebt jemand einen Pfad in der tier-map hoch und vergisst CODEOWNERS, ist die Voraussetzung still gebrochen, und ein T2-PR mergt ungeprüft. Der Wächter ist check-codeowners, der diese Invariante (tier-map ⊆ CODEOWNERS) fail-closed prüft. Das ist das Prinzip, das den ganzen Abschnitt trägt: Eine Voraussetzung, auf die man sich verlässt, wird selbst zum Gate, nicht zum Merksatz.

5. Metrik-Drift: die Zahlen rutschen, einen Punkt nach dem anderen. Die fünfte ist die langsamste und tückischste: Kein einzelner PR senkt die Qualität sichtbar, aber Monat für Monat rutscht eine Kennzahl ein halbes Prozent, und irgendwann ist der Boden weich. Die schärfste Antwort darauf ist ein Ratchet, eine Schwelle, die nur in eine Richtung nachgibt. Obol fährt das beim Mutations-Score schon am Anschlag: Die Schwelle steht auf hundert Prozent (break: 100), ein einziger überlebender Mutant rötet die CI. Eine Metrik, die oben festgenagelt ist, kann nicht abrutschen. Der allgemeinere Ratchet, eine Schwelle, die sich automatisch auf den jeweils erreichten Stand nachzieht, ist die Ausbaustufe für Kennzahlen, die noch nicht am Anschlag stehen, etwa die Zeilenabdeckung. Wo Obol das noch nicht durchgängig nutzt, sage ich es offen, aber das Prinzip ist gesetzt: Eine Schwelle, die nachgeben kann, gibt irgendwann nach.

Fünf Arten zu verrotten, fünf Antworten, und ein gemeinsames Prinzip darunter: Ein Käfig, der nur einmal gebaut wird, ist schon beim Bau veraltet. Was ihn frisch hält, ist nicht ein besonders kluges Gate, sondern dass für jede Erosionsrichtung jemand zuständig ist, und dass die Wächter sich gegenseitig die Voraussetzungen bewachen.

Jetzt, und keinen Schritt früher, darf der Agent rein.

Der Agent zieht ein

Der Käfig steht und ist bewiesen. Jetzt der Insasse. Den Agenten orchestriert das devloop-Plugin durch vier Stationen, und der Witz ist, dass keine davon der anderen traut.

Installieren. In Claude Code:

/plugin marketplace add mayflowergmbh/devloop
/plugin install devloop@devloop

Verdrahten. /devloop:init legt den Bindungs-Anker (den precondition-check aus Schicht 5) ins Repo und meldet den Käfig beim Plugin an. Das ist die Naht zwischen den zwei Hälften: Ab hier weiß der Agent, in welchem Käfig er sitzt.

Die vier Stationen. Ein Lauf wandert durch specifyspec-to-testsimplementcritic, und der loop-Treiber führt sie als isolierte Subagenten aus. Das ist kein Zierrat, das ist Anti-Kollusion: Jede Station bekommt frischen Kontext und sieht nicht, womit die vorige sich herausgeredet hat. Der implement-Schritt darf Spec und Tests nur lesen, nicht ändern. Und an den heiklen Übergängen sitzen harte menschliche Stopps. Der Agent ist also nicht ein großer Prompt, der alles auf einmal macht, sondern eine Kette aus Spezialisten mit eingebauter Gewaltenteilung, das Prinzip aus Teil 5, eine Ebene kleiner.

(Die genauen Kommandos und Optionen stehen in der USAGE.md des Plugins; ich halte mich hier an den Ablauf, nicht an jede Flagge.)

Den Agenten autonom laufen lassen

Warum. Ein Agent, der unbeaufsichtigt arbeiten soll, braucht eine gekapselte Ausführungs-Umgebung. Sonst hat der Prozess, der gleich selbstständig Code schreibt und Befehle ausführt, dieselben Rechte auf deiner Maschine wie du. Wichtig, und leicht zu verwechseln: Das ist nicht der Merge-Käfig. Der Merge-Käfig schützt main vor schlechtem Code. Die Ausführungs-Eindämmung schützt deine Maschine vor dem Agenten, während er arbeitet. Zwei verschiedene Käfige, beide nötig (Teil 5).

Zwei Wege.

  • Der Sandbox-Mode von Claude Code (.claude/settings.json): leichtgewichtig und nativ. Du ziehst Schreib- und Lesegrenzen, eine Domain-Allowlist fürs Netz (network.allowedDomains), erlaubte Lesepfade (filesystem.allowRead) und Ausnahmen für einzelne Befehle. Der Agent läuft auf deiner Maschine, aber eingehegt.
  • Ein Dev Container (Docker): volle Isolation in einem eigenen Container mit eigenem Dateisystem- und Netz-Rand. Mehr Aufwand im Setup, aber sauberer für echten unbeaufsichtigten Betrieb und näher an der CI-Umgebung.

Beim ersten Bot-Lauf lief die Eindämmung gegen ihre eigene Identität. Der Schlüssel der GitHub App lag unter ~/.ssh und war damit nicht lesbar, api.github.com fehlte in der Allowlist, und selbst danach kamen die üblichen Werkzeuge nicht durch: gh und Nodes fetch brechen unter der Sandbox an der TLS-Prüfung gegen den System-Truststore, curl und git bringen ihren eigenen mit und funktionieren. Die Lehre ist die aus Teil 5 in Reinform: Eine pauschale Schutzgrenze trifft genau den legitimen Pfad des autorisierten Bots. Die Grenze muss den erlaubten Pfad gezielt ausnehmen (Schlüssel-Ort, Domain-Allowlist, Wahl des Clients), statt ihn mitzusperren. Beim ersten Lauf kostete genau das einen guten Teil eines Tages, bis die Schichten aufeinander abgestimmt waren.

Und hier kommt das Prinzip zurück, das uns schon beim zurückgefallenen PR begegnet ist: Alles, was an der lebenden Session hängt, ist für unbeaufsichtigten Betrieb fragil. Der Agent kann seinen PR beobachten, nachziehen, auf Gates reagieren, aber nur, solange seine Session läuft. Was die Autonomie trägt, wenn niemand mehr zusieht, ist nicht der Agent, sondern der Käfig: Der Auto-Merge vollzieht sich serverseitig, ob die Session noch lebt oder längst beendet ist. Deshalb sitzt der Merge auf GitHubs Servern und nicht in der Agent-Shell. Der Agent ist der Arbeiter, aber der Käfig ist das, was bleibt.

Der erste Lauf: T1

Fang klein an, mit etwas, das nichts kaputt machen kann. Eine read-only-Änderung, ein Stück isolierte Logik, eine Doku-Ergänzung. Du gibst dem Agenten die Aufgabe, und er wandert durch die Stationen: Aus der Beschreibung wird eine Spezifikation, daraus Tests, daraus Code, über den der critic am Ende mit frischen Augen liest. Dann öffnet der Bot einen PR.

Und jetzt der Moment, für den der ganze Käfig gebaut wurde: Der PR berührt keinen Owner-Pfad, also verlangt GitHub kein Review. Die Gates laufen, sie werden grün, und der Auto-Merge schließt den PR, ohne dass du ihn je angesehen hast. Du warst nicht im Spiel. Das ist kein Kontrollverlust, das ist die eingelöste Staffel: Bei einer Änderung dieser Klasse ist deine Aufmerksamkeit zu wertvoll, um sie zu verlangen.

Der zweite Lauf: T2

Jetzt ein echtes Feature im Service-Code. Hier zeigt sich, warum der Ablauf nicht „erst alles bauen, dann ein PR“ ist, sondern in zwei PRs zerfällt. Der Grund ist ein Stolperstein, der die ganze Stationen-Trennung fast gesprengt hätte.

Die specify-Station legt ihre Spezifikation unter .specify/ ab, und das steht im geschützten Satz (Schicht 2). Ein einziger PR, der Spec, Tests und Code zusammenbringt, berührt damit zwangsläufig den geschützten Satz, jedes Feature bräuchte einen Admin-Override allein dafür, seine eigene Spec mitzutragen. Die Auflösung heißt Spec-PR-first und ist clever: Die Spec kommt zuerst, allein, mit den Tests, die alle auf .skip stehen. main bleibt grün, weil das Trace-Gate die Skips zählt und die Testsuite sie nicht als rot wertet. Erst danach entfernt implement die .skip und liefert den Code.

Daraus wird ein Ablauf mit genau zwei menschlichen Toren, und beide sitzen dort, wo Teil 7 sie vorhergesagt hat:

  1. Der Spec-PR. Der Agent legt Spezifikation und übersprungene Tests vor. Weil .specify/ ein Owner-Pfad ist, landet er auf deinem Tisch. Das ist das Intent-Tor: Bevor eine Zeile Produktionscode entsteht, entscheidest du, ob das Beschriebene das Richtige ist. Genau die Frage, die kein Gate beantworten kann.
  2. Der Implement-PR. Nach deiner Freigabe entfernt der Agent die .skip und schreibt den Code. verify-unskip wacht darüber, dass er dabei wirklich nur aktiviert und nicht heimlich die Tests umschreibt. Dieser PR berührt den Service-Code, also wieder einen Owner-Pfad, also dein zweites Review: das unabhängige Augenpaar vor dem Merge. Hier fasst du das Feature an, auf der Testumgebung, prüfst es gegen die Absicht und gibst frei.

Drei Gates haben mit den geskippten Spec-Tests zu tun, und nur ein Idiom erfüllt alle. Der Escape-Hatch-Scanner aus Schicht 3 verbietet jeden übersprungenen Test, dessen Titel kein REQ-…-Tag trägt: ein nacktes .skip ist eine Fluchttür, ein REQ-getaggter Platzhalter ist erlaubt. verify-unskip wertet pro Einzeltest und ignoriert den describe-Container. Und das Trace-Gate zählt die REQ-Tags pro Test als Abdeckung. Daraus folgt genau eine Form: ein ungeskipptes describe(...), in dem jeder Einzeltest als it….skip("REQ-…: …") mit seinem Tag im Titel steht. Ein describe.skip über die ganze Suite trägt keine Per-Test-Tags, zählt nicht als Abdeckung und fliegt als Fluchttür raus. Halt dich ans Idiom deines ersten Features, dann threadest du die Nadel von selbst.

Dazwischen lief die Maschine allein. Das ist kein konstruiertes Beispiel, genau so ist das erste echte Feature durch den fertigen Käfig gegangen.

Nicht jeder Lauf segelt so durch, und das ist gut zu wissen. Bei einem späteren Feature steckte ein Defekt nicht im Code, sondern in einer Testvorrichtung: Ein Test räumte eine Datenbanktabelle ab, von der ein anderer abhing. Solange die Tests .skip‚t waren, blieb das unsichtbar. Der Spec-Review sah die Zusicherungen, nicht das Laufverhalten, und gab frei. Rot wurde es erst, als implement die Tests aktivierte.

Und jetzt das Lehrreiche: Der Loop konnte den Defekt nicht selbst heilen, weil implement Tests nur aktivieren, nicht reparieren darf. Nach ein paar fruchtlosen Runden eskalierte er an einen Menschen, und die Korrektur lief über einen frischen Spezifikations-Zyklus. Das ist kein Makel, das ist die Eskalation aus Teil 7 in Aktion: nicht der hundertste Versuch, sondern der saubere Übergabepunkt. Es zeigt zugleich die eine ehrliche Grenze von Spec-PR-first: Das Intent-Tor prüft, ob das Beschriebene das Richtige ist, nicht, ob die Testvorrichtung trägt. Der Prüfstein dafür ist der Unskip im Implement-PR.

Was der Käfig nicht leistet

Bevor das zu glatt klingt: Der Käfig nimmt dir das Misstrauen nicht ab, er verschiebt es. Vier Stellen, an denen du das wissen solltest.

  • Er ist nur so gut wie die tier-map. Ein T1-PR mergt ungesehen, im Vertrauen darauf, dass der Pfad ihn zu Recht als harmlos führt. Sitzt ein gefährlicher Default an einer Stelle, die die tier-map zu niedrig einstuft, geht er automatisch durch. Der Pfad ist ein Stellvertreter fürs Risiko, kein Maß davon, deshalb kalibrierst du konservativ und stufst im Zweifel hoch. Aber eine Restwette auf die Einstufung bleibt.
  • Er erzwingt, dass ein Mensch gefragt wird, nicht, dass er hinsieht. Das T2-Tor ist erfüllt, sobald ein Owner auf „approve“ klickt. Ob er das Feature wirklich gegen die Absicht geprüft oder nur durchgewinkt hat, kann kein Gate erzwingen. Das ist die eine Stelle, an der „durchgesetzt“ wieder zu „versprochen“ wird, und sie ist unvermeidbar: Intent ist genau das, was sich nicht automatisieren lässt (Teil 2 und 7). Der Käfig sorgt dafür, dass die Frage gestellt wird. Die Antwort bleibt menschlich.
  • Er verschiebt den menschlichen Aufwand, er halbiert ihn nicht. T1 verschwindet von deinem Tisch, das ist der Gewinn. Aber jedes T2/T3-Feature kostet dich jetzt zwei Tore statt eines klassischen Reviews, den Spec-PR und den Implement-PR. Der Netto-Gewinn hängt davon ab, wie groß der T1-Anteil ist. Eine Domäne, die fast nur aus T2/T3 besteht, macht den Menschen zum Engpass, und das ist gewollt (irreversibel schlägt schnell), aber es ist kein „weniger Arbeit“, sondern „Arbeit an der richtigen Stelle“.
  • Der autonome Teil braucht verlässlich grüne Gates. Der T1-Auto-Merge steht und fällt damit, dass die Checks deterministisch sind. Flaky Tests, gerade Integrationstests gegen echte Container, untergraben ihn doppelt: Sie blockieren harmlose PRs grundlos und erzeugen Druck, die Schwelle aufzuweichen, also genau die Metrik-Drift aus der Vertiefung oben. Ein Mutations-Gate auf hundert Prozent ist nur tragbar, solange der geprüfte Kern wirklich deterministisch ist.
  • Und schließlich die ehrlichste Grenze: Das alles lohnt sich nicht überall. Für einen Wochenend-Prototyp, ein Repo mit zwei Wochen Lebensdauer oder einen Solo-Entwickler ohne eine zweite Instanz für echte Gewaltenteilung ist der ganze Apparat überdimensioniert. Der Käfig zahlt sich dort aus, wo drei Dinge zusammenkommen: agentisches Tempo, ein Bedarf an Mehraugen-Kontrolle und Code, der lange genug lebt, um zu verrotten. Fehlt eines davon, schlägt der Aufwand den Nutzen.

Was du jetzt hast

Erinnerst du dich an den Feierabend vom Anfang, an dem du den Diff Zeile für Zeile gelesen hast, weil du nicht vertrauen konntest?

Den gibt es nicht mehr. Nicht, weil du dem Agenten jetzt vertraust, sondern weil du nicht mehr musst. Die triviale Änderung ist gemerged, ohne dass du sie gesehen hast, und das ist in Ordnung, der Käfig hat sie geprüft. Das gewichtige Feature liegt auf deinem Tisch, zweimal: einmal als Frage nach der Absicht, einmal als letzter Blick vor dem Merge. Du liest nicht mehr jeden Diff. Du liest die, die der Käfig dir vorlegt, und du liest sie aus dem richtigen Grund, weil dort ein Mensch gebraucht wird, nicht weil du einem Verdacht nachgehst.

Das ist die ganze Idee dieser Serie, in einem Repo zusammengebaut: nicht ein Agent, dem man glauben muss, sondern ein System, das die Frage nach dem Glauben überflüssig macht. Den Rest, das, was kein Gate je abnimmt, behältst du. Er ist kleiner geworden und wichtiger. Worum es bei diesem Rest genau geht, steht in Teil 7; welche Gates für deine Domäne sinnvoll sind, hängt an deinem Stack und ist ein Thema für sich; und die drei Vertiefungen im Anhang unten gehen tiefer.

Der Käfig steht, der Agent arbeitet, und dein Feierabend gehört wieder dir. Nicht, weil dich niemand mehr braucht, sondern weil du nur noch dort gebraucht wirst, wo es darauf ankommt: bei der Absicht, beim Unumkehrbaren, beim Käfig selbst. Das Zeile-für-Zeile-Lesen aus Misstrauen übernimmt keiner mehr, weil es keiner mehr muss.

Durchgesetzt schlägt versprochen.

Anhang: Vertiefungen

Drei Stellen, an denen der Käfig mehr verdient als einen Halbsatz. Wer nur nachbauen wollte, ist oben fertig; wer die Gates von innen sehen will, liest hier weiter. Nichts davon hängt am Plugin, es ist GitHub und Standard-Werkzeug.

Der Boden im Detail: was Mutation fängt und Coverage nicht

Testabdeckung beantwortet eine schwächere Frage, als die meisten denken. Sie sagt, dass eine Zeile beim Testlauf ausgeführt wurde, nicht, dass ein Test es gemerkt hätte, wäre die Zeile falsch. Ein it("funktioniert", () => { berechne(2,2); }) ohne ein einziges expect bringt die Coverage nach oben und prüft nichts. Mutationstesten schließt genau diese Lücke: Es verändert den Code gezielt (aus + wird -, aus > wird >=) und schaut, ob ein Test daraufhin rot wird. Überlebt eine Mutation, ist die Stelle zwar abgedeckt, aber nicht abgesichert.

Obol zieht daraus eine bewusste Trennung. Die reine Domänenlogik liegt in *.pure.ts-Modulen, seiteneffektfrei, und wird vollständig mutationsgetestet, mit einer harten Schwelle (jeder überlebende Mutant rötet die CI; die Property-Tests mit fast-check erschlagen die Arithmetik-Mutanten). Der I/O-Rand dagegen, Datenbank und HTTP, wird nicht mutationsgetestet (das wäre pro Mutant viel zu teuer), sondern mit Integrationstests abgedeckt. Das ist die Framework-Regel „zieh die Risikologik aus dem I/O heraus“ in Werkzeugform: ein prüfbarer reiner Kern, eine dünne Schale drumherum.

Und die Schale wird gegen die Wirklichkeit getestet, nicht gegen eine Annahme. Obols Integrationstests laufen gegen ein echtes Postgres in einem Wegwerf-Container (Testcontainers, postgres:16-alpine) und einen echten HTTP-Server, nicht gegen Mocks. Ein Mock prüft deinen Code gegen deine Vorstellung davon, wie Postgres sich verhält; ein Container prüft ihn gegen Postgres. Die Differenz zwischen beiden ist genau die Sorte Fehler, die sonst erst in Produktion auftaucht.

Die Agenten-Identität als Muster: kurzlebige Tokens statt Geheimnis im Dotfile

Die GitHub App aus Schicht 1 ist mehr als ein Trick gegen den Self-Approval-Deadlock, sie ist ein Muster für Maschinen-Identität. Drei Eigenschaften lohnen den Blick.

Erstens: Das einzige langlebige Geheimnis ist der private Schlüssel, und der liegt außerhalb des Repos, chmod 600. Alles andere ist flüchtig. Die App signiert lokal ein kurzlebiges JWT und tauscht es gegen ein Installations-Token, das nach etwa einer Stunde verfällt. Es liegt kein Dauer-Token in einem Dotfile und wartet auf sein Leak. Zweitens reden alle Aufrufe über curl und git, nie über gh oder Nodes fetch. Der Grund ist die Sandbox: gh (ein Go-Binary) und fetch prüfen TLS gegen den System-Truststore, den die Sandbox blockt, während curl und git ihren eigenen mitbringen. Schlüssel und Token reichen über eine stdin-Config (--config -) hinein, nie über die Kommandozeile, wo sie in der Prozessliste stünden. Drittens die Capability-Grenze aus Schicht 1: kein Workflow-Recht, also kann selbst ein kompromittiertes Token die geänderten CI-Gates nicht nach main pushen.

Das Muster, eine Ebene über Obol: Gib dem Agenten einen eigenen Principal, die minimal nötigen Rechte, kurzlebige Anmeldedaten, halte das eine langlebige Geheimnis außer Reichweite, und entwirf den Netzpfad für die Sandbox, statt sie abzuschalten. Eine Grenze, die man für die legitime Arbeit deaktiviert, ist keine Grenze mehr.

Spec zu Test: der trace-coverage-Mechanismus

Das leiseste Driften ist das zwischen Spezifikation und Code. Beide starten deckungsgleich, dann lebt der Code weiter, und irgendwann beschreibt die Spec ein Programm, das es nicht mehr gibt, ohne dass je etwas rot geworden wäre. trace-coverage macht dieses Driften laut, und der Mechanismus ist bewusst unspektakulär.

Jedes EARS-Kriterium in der spec.md trägt eine ID der Form REQ-BAL-01. Dieselbe ID steht im Namen des Tests, der das Kriterium prüft (it("REQ-BAL-01: lehnt negative Beträge ab", …)). Das Gate ist danach pure Zeichenkettenlogik: Es sammelt alle IDs aus der Spec und alle aus den Testquellen und vergleicht zwei Richtungen. Ein Kriterium, das kein Test referenziert, ist eine Lücke (untested) und macht das Gate rot. Eine Test-Referenz auf eine ID, die keine Spec kennt, ist ein Waise (orphan), also ein Tippfehler oder ein gelöschtes Kriterium. Sauber ist der Trace nur, wenn jedes Kriterium getestet und keine Referenz ins Leere zeigt. Ein Test darf dabei mehrere Kriterien abdecken.

Der Gewinn ist doppelt. Erstens wird Spec-Drift zu einem fehlschlagenden Build statt zu einer vergessenen Notiz: Wächst der Code über ein Kriterium hinaus, bleibt der referenzierende Test rot. Zweitens ist genau dieser Mechanismus es, der den Spec-PR-first-Ablauf aus dem T2-Lauf trägt: Die zunächst .skip-ten Tests referenzieren ihre Kriterien bereits, der Trace zählt sie, und main bleibt grün, während die Implementierung noch aussteht. Das ist der Spec-Intent-Faden aus Teil 2, zu einem Gate gehärtet: nicht „existiert eine Spec“, sondern „deckt sich das Laufende noch mit dem Beschriebenen“.

Gefällt dir, wie wir über KI denken?

Diese drei Produkte sind genau aus diesem Denken heraus entstanden. Jedes davon aus einem direkten Kunden-Need heraus, nicht aus einer Pitch-Deck-Session. Wenn eines davon zu deiner aktuellen Baustelle passt: Ein Gespräch. Kostenlos. Kein Commitment.

Dein Kontakt zu uns

Avatar von Tobias Mogdans

Dein Thema?

Das Thema interessiert dich? Wenn Du fragen hast, dann melde dich ganz unverbindlich bei uns!

Wie dürfen wir Dich ansprechen?

Höflichkeit ist uns wichtig!

Wie können wir Dich erreichen?

Womit können wir behilflich sein?


Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Für das Handling unseres Newsletters nutzen wir den Dienst HubSpot. Mehr Informationen, insbesondere auch zu Deinem Widerrufsrecht, kannst Du jederzeit unserer Datenschutzerklärung entnehmen.