Inhalt

Dieser Text beschreibt eine Pipeline, die bei uns produktiv läuft - an einem mandantenfähigen Kommunikationssystem auf ASP.NET Core, Angular und FreeSWITCH, mit selbst gehostetem GitLab. Alle unten genannten Befunde stammen aus echten Durchläufen und wurden vor dem Merge gefunden und behoben. Das System benennen wir nicht, weil einzelne Befunde sonst rückverfolgbar wären.

Was ist der eigentliche Engpass beim Entwickeln mit KI?

Nicht die Geschwindigkeit, mit der Code entsteht. Der Engpass ist Vertrauen: Code, der kompiliert, dessen Tests grün sind und der sich beim Lesen richtig anfühlt - und trotzdem falsch ist. Diese Fehlerklasse macht fast jeden Fund aus, den wir haben, und keine Standardpipeline sieht sie.

Sie hat bei uns einen Namen bekommen, weil sie so verlässlich wiederkehrt:

Etwas schützt nicht, und nichts sagt es.

Eine Pipeline ist grün. Ein Riegel greift nicht. Ein Prüfer prüft etwas anderes, als sein Name behauptet. Genau dagegen ist die folgende Pipeline gebaut - nicht gegen langsames Tippen.

Wie sind die Rollen verteilt?

Vier Rollen mit unterschiedlichen Modellen und unterschiedlichen Rechten. Die Rechtetrennung ist die tragende Konstruktion, nicht die Modellwahl.

RolleModellRechteAufgabe
Team-Lead (Orchestrator)OpusGitLab-Token, git, CI, Deployschneidet Tickets, spawnt Agenten, führt Reviews zusammen, merged
ImplementiererOpuseigener Worktree, kein Tokenbaut eine Scheibe, committet, pusht nicht
Reviewer „Quality"Fableread-onlyKorrektheit, Testqualität, Repo-Regeln
Reviewer „Security"Opusread-onlyThreat-Model, Rechte, Egress, Mandantentrennung

Zwei Trennungen sind nicht kosmetisch:

Implementierer haben keinen GitLab-Zugang. Sie committen in einen isolierten Worktree und melden einen Hash. Push, Merge Request, Labels, Merge und Deploy macht ausschließlich der Lead. Das erzwingt, dass jede Änderung durch genau ein Nadelöhr geht - und dass niemand seine eigene Arbeit durchwinkt.

Reviewer dürfen nichts ändern. Sie finden und beweisen, sie reparieren nicht. Ein Reviewer, der nebenbei repariert, hört auf zu suchen. Das ist keine Vermutung, sondern der Grund, aus dem auch Menschen ihren eigenen Code schlecht reviewen.

Wie läuft ein Ticket durch die Pipeline?

Ticket (Definition of Ready)
  └─ Triage: umsetzbar? → InProgress | nicht umsetzbar? → DoR entfernen + Kommentar, was fehlt
      └─ Seams kartieren (read-only Agent)
          └─ Implementierer im eigenen Worktree → Commit
              └─ Lead pusht → feature/<n> → Draft-MR gegen den Integrationsbranch
                  └─ Review-PAAR über `git diff <base>...<tip>`
                      └─ Jeder Befund als Inline-Diskussion auf der Zeile + Urteil als MR-Notiz
                          └─ EIN priorisierter Fix-Batch an DENSELBEN Implementierer
                              └─ ⟳ Runde 2 über den FIX-DIFF (nicht den Gesamt-MR)
                                  └─ Pipeline grün → undraft → approve → merge
                                      └─ Voller CI-Lauf auf dem Integrationsbranch → Deploy UAT
                                          └─ Prüfnotiz ans Ticket → Label UAT

Der Punkt dabei: die Review lebt auf dem Merge Request, nicht im Chatverlauf. Jeder Befund mit Zeilenbezug wird eine auflösbare GitLab-Diskussion an genau der Zeile - mit konkretem Szenario, nicht mit „könnte problematisch sein". Wer den MR in einem halben Jahr öffnet, sieht Fund → Reparatur → Auflösung, so wie bei einem menschlichen Reviewer. Für Abnahmen in Umgebungen mit Nachweispflicht ist das der entscheidende Unterschied zwischen einem Werkzeug und einem Verfahren.

Warum wird nach jedem Fix erneut geprüft?

Weil Reparaturen eine eigene Fehlerklasse haben. Die wichtigste Regel unserer Pipeline, und die, die am meisten gefunden hat:

Nach jedem Fix-Batch läuft die Review erneut - über den Fix-Diff, nicht über den Gesamt-MR - mit zwei anderen Fragen: Was hat die Reparatur kaputt gemacht? Und sind die Regressionstests echt?

An einem einzigen Arbeitstag hat diese Runde dreimal denselben Musterbefund erzeugt: Die Reparatur schließt eine Achse und reißt die Nachbarachse auf.

  • Ein Riegel wurde am Angebots-Gate eingebaut, das Aufruf-Gate blieb offen - obwohl der Autor denselben Fehler zwei Absätze weiter oben selbst dokumentiert hatte.
  • Ein Seed war auf der Host-Achse geschlossen und auf der Mandanten-Achse offen, während die Architekturdokumentation ihn als geschlossen führte.
  • Vor einer Reparatur konnte Fremdverkehr eine Prüfung nur grüner machen; danach konnte ein anonymer Fremder sie dauerhaft auf Gelb nageln.

Nach maximal drei Runden wird nicht weiter iteriert. Dann entscheidet ein Mensch. Iterationen, die nach der dritten Runde noch Befunde produzieren, sind ein Signal über den Ticketschnitt, nicht über den Code.

Wann gilt ein Regressionstest als echt?

Wenn belegt ist, dass er fehlschlägt, sobald man den Fix zurücknimmt. „Test hinzugefügt" ist keine Aussage. „Mutation X eingebaut, N Tests rot, Mutation zurückgenommen, alle grün" ist eine. Der Nachweis gehört an den Befund, nicht in die Commit-Nachricht.

Das klingt nach Bürokratie, bis man die Trefferquote sieht. Vier Beispiele aus echten Durchläufen:

  • Ein Prüfer für „das Geheimnis darf nicht im Audit-Log stehen" ließ die Regression in 45 % der Läufe durch. Ursache: Der zufällige Base64-Schlüssel enthält mit rund 49 % Wahrscheinlichkeit ein +, und der JSON-Serializer escapt genau das - die Substring-Prüfung traf dann nie. 22 Läufe zur Messung, danach eine strukturelle Prüfung: Eigenschaft vorhanden, ja oder nein.
  • Ein Nebenläufigkeitstest tötete seinen Mutanten in 5 von 30 Läufen, weil das Zeitfenster nach dem ersten Treffer nie wieder aufging. Ersatz mit Barriere über 1000 Runden: 30 von 30.
  • Ein Prüfer suchte einen zurückgekehrten Fremdstring nur in öffentlichen Properties - der einzige realistische Speicherort wäre ein privates Feld gewesen.
  • Fünf Prüfer eines „Härtungs"-Commits blieben bei eingebauter Mutation allesamt grün. Der Commit existierte, weil dieselben Prüfer schon einmal zu wenig gemessen hatten.

Ein Test, der nie rot war, ist keine Absicherung. Er ist eine Behauptung mit grüner Farbe.

Welche Fehlerklassen wiederholen sich?

Jede Fehlerklasse, die zweimal auftritt, wird aufgeschrieben und in jeden Review-Auftrag mitgegeben. Die Liste ist das Gedächtnis der Pipeline - Agenten haben keines. Ein Auszug:

  • Registrierung nach Namenskonvention greift nur bei passendem Klassennamen. Sonst ist die aufgelöste Liste leer, und die Prüfung meldet zufrieden „nichts registriert".
  • „Nicht rot" fängt keinen toten Prüfer. ShouldNotBe(Fail) besteht auch bei Unknown. Und typeof(X).ShouldNotBeAssignableTo<Y>() prüft das Type-Objekt - immer grün.
  • Mutationsproben gegen alte Artefakte messen nichts. Zweimal an einem Tag: einmal per mv zurückgestellt (älterer mtime, MSBuild baut nicht neu), einmal scheiterte die Mutation am Compiler und die „grünen Läufe" waren No-ops.
  • Der Zwilling hat den Riegel, dieses Exemplar nicht. Der teuerste Einzelfund eines Tages war genau das: Ein Geheimnis landete im Klartext im Audit-Log, während das passende Attribut in fünf Schwester-DTOs danebenstand - eines davon mit dem Kommentar „darf NICHT ins Audit-Log serialisiert werden".

Warum ist CI das Gate und nicht der Worktree?

Weil ein grüner Teil-Lauf im Worktree nichts über das Zusammenspiel aussagt. Gemerged wird auf grüner Pipeline, und der Integrationsbranch fährt danach noch einmal die volle Suite, die auch den UAT-Deploy gatet.

Dazu gehört, die Pipeline ehrlich zu halten:

  • Schwere Jobs sind serialisiert (resource_group). Vier parallele Feature-Pipelines haben einen 25-Minuten-Lauf auf über eine Stunde gedehnt - zwei Jobs starben bei exakt 3607 Sekunden im Runner-Cap. Vier serielle Läufe, die durchlaufen, sind billiger als vier parallele, die alle scheitern.
  • Überholte Pipelines werden abgebrochen (interruptible), Jobs mit Außenwirkung ausdrücklich nicht.
  • Ein Timeout ist kein Testfehler. Die Unterscheidung muss man aktiv treffen, sonst „repariert" man Code, der nie kaputt war.

Warum wird Zustand zurückgelesen statt angenommen?

Weil ein erfolgreicher Schreibaufruf kein Beweis ist. Der gelesene Zustand ist einer. Ticket-Labels (DoRInProgressUAT) setzt bei uns ein kleines Skript, das beide Seiten in einem Aufruf ändert und den Zustand danach ausliest.

Das Skript entstand, nachdem der Lead die Regel dreimal von Hand verletzt hatte. Es hatte danach selbst zwei Fehler: einen Abbruch genau in der Verifikation, und eine Prüfung, die wegen Paginierung nur die ersten 100 Tickets ansah und trotzdem „alles in Ordnung" meldete. Dieselbe Fehlerklasse, die das Werkzeug verhindern sollte, im Werkzeug.

Wir halten das für den ehrlichsten Beleg dafür, dass die Fehlerklasse nichts mit KI zu tun hat. Sie wird durch Geschwindigkeit nur häufiger.

Welche Entscheidungen bleiben beim Menschen?

Architektur, Security und Release - plus jede blockierende Frage. Blockierende Fragen gehen als Ticket-Kommentar raus, mit zwei bis drei Optionen, einer Empfehlung und der Angabe, was ohne Antwort passiert. Nicht als Chat-Nachricht, die im Verlauf verschwindet.

Harte Regeln, die kein Agent aufweicht:

  • Produktionsdaten werden gelesen, nicht geschrieben. Konfigurationsänderungen benennt der Agent als GUI-Feld, gesetzt wird von Hand.
  • Zugangsdaten stehen nur in einer Datei außerhalb des Repositories und werden inline gelesen - nie in Commit, MR, Kommentar oder Log.
  • Kein Push auf geschützte Branches. Kein Deploy ohne grüne Pipeline.

Was hat die Pipeline tatsächlich gefunden?

Eine Auswahl aus einem Durchlauf, alle vor dem Merge:

FundWarum keine Pipeline ihn gesehen hätte
Eingegebenes Geheimnis landet im Klartext im Audit-Log - und damit in jeder künftigen SicherungAlle Tests grün; der Riegel stand fünfmal daneben, nur hier nicht
Ein Selbsttest meldet grün, während die überwachte Funktion tot ist - der betroffene Zweig endete in Unknown, und Unknown fällt aus der GesamtbewertungGenau der Fall, für den die Prüfung gebaut war
Vier Backend-Oberflächen liefen still in die Single-Page-App: HTTP 200 mit index.html statt der APIDer Client prüfte IsSuccessStatusCode - bei 200 wahr - und meldete „Tunnel unten"
Ein Werkzeug für den KI-Assistenten hätte eine komplette Anlagen-Wiederherstellung als harmlose Schreiboperation angebotenKein Rechtefehler - das Bestätigungshäkchen hätte das Modell gesetzt
Eine Reparatur legte den Aufräum-Mechanismus dauerhaft still: Nach dem ersten Einsatz lief das Volume voll - und ein volles Volume verhindert den nächsten EinsatzDer Test prüfte den Normalfall, nicht den Dauerbetrieb
Die Prüfung zählte mandantenübergreifend, der Aufräum-Pfad war mandantengefiltert: „2 Einträge → bitte löschen", Liste leer, IDs 404Beide Seiten für sich korrekt

Ein Muster ist dabei so verlässlich, dass es inzwischen als Suchheuristik dient: Wenn ein Riegel ersatzlos zu löschen den Build bricht, ist die gefährliche Mutation nicht das Weglassen, sondern die verwechselte Bedingung.

Was hat nicht funktioniert?

Ehrlichkeit gehört dazu, sonst wird es Werbung.

  • Einweg-Wecker auf einzelne Pipelines. Jeder neue Push erzeugt eine neue ID, auf die niemand wartet. Ersetzt durch eine dauerhafte Beobachtung des Projekts.
  • Überlast statt Parallelität. Mehr gleichzeitige Agenten heißt nicht mehr Durchsatz, wenn alle auf dieselbe Maschine drücken. Ein Agent hat beim Aufräumen fremde Testläufe abgeschossen.
  • Der Lead ist nicht immun. Mehrere Fehlvorgaben in Fix-Batches wurden von den Implementierern widerlegt - mit Messung. Eine davon hätte eine Regression eingebaut, eine andere hätte einen Prüfer von zuverlässig auf zufällig gestellt. Dass Implementierer widersprechen dürfen und belegen müssen, ist Teil der Sicherung.
  • Diagnosen, die eine von mehreren Ursachen behaupten, kosten Stunden. „Tunnel unten" war die Meldung; die Ursache war eine fehlende Route im Reverse-Proxy. Fehlermeldungen nennen jetzt Statuscode und Content-Type statt einer Vermutung.

Die kurze Fassung

  1. Trennung der Rechte: Wer baut, pusht nicht. Wer prüft, ändert nicht.
  2. Zwei Reviewer, zwei Modelle, zwei Blickwinkel - auf denselben Diff.
  3. Jeder Befund landet als auflösbare Diskussion auf der Zeile, nicht im Chat.
  4. Nach jedem Fix wird der Fix geprüft, mit anderen Fragen als beim ersten Mal.
  5. Ein Test zählt, wenn er nachweislich fehlschlägt, sobald man den Fix zurücknimmt.
  6. Wiederkehrende Fehlerklassen werden aufgeschrieben und in jeden Auftrag mitgegeben.
  7. Zustand wird zurückgelesen. Ein erfolgreicher Schreibaufruf ist kein Beweis.
  8. Nach drei Runden entscheidet der Mensch, nicht die nächste Iteration.

Der Aufwand ist real: Ein Feature durchläuft leicht drei Review-Runden, und jede Runde kostet. Der Gegenwert ist auch real - es sind durchweg Befunde, die eine grüne Pipeline nicht liefert.

Das ist auch die Antwort auf die Frage, die uns Kunden zum Thema KI am häufigsten stellen. Die freigewordene Kapazität geht bei uns in Testtiefe, nicht in mehr Features. Wie wir das in bestehende Teams und Toolchains einführen, steht unter KI-Engineering; der Rahmen für regulierte Projekte unter Welche Leitplanken wir Agenten setzen und Qualitätssicherung & Testautomatisierung.