Die Hände einer Person an einer Werkbank: Die eine hält ein Fitting aus Messing, die andere schreibt mit einem roten Bleistift auf ein Klemmbrett, daneben stehen weitere Fittings. Ein Sinnbild für Verifikation: Wer wissen will, ob ein Teil stimmt, muss es prüfen und festhalten, was dabei zu beobachten war.
Ein einzelnes Teil prüfen und aufschreiben, was es tut. Das Teil herzustellen ist nicht mehr der Aufwand.

Im August hat Fabien Potencier Pull Requests für das Repository von Symfony Language Tools abgeschaltet. Kurz darauf hat Taylor Otwell GitHub Issues für die meisten Laravel-Pakete abgeschaltet. In den Kommentaren unter Fabiens Ankündigung wurden die beiden Entscheidungen bald als gegensätzliche Schritte beschrieben.

Ich lese sie als dieselbe Schlussfolgerung. Beide haben festgestellt, dass der Diff zum billigen Teil eines Beitrags geworden ist. Uneinig sind sie sich nur darüber, welcher Tab auf GitHub bestehen bleiben soll. Wichtiger finde ich eine Frage, die keine der beiden Ankündigungen beantwortet: In welcher Form erreicht das Verständnis eines Problems ein Projekt, wenn niemand mehr etwas verstehen muss, um einen Diff zu erzeugen? Meine Antwort ist ein fehlschlagender Test.

Beide gehen von derselben Annahme aus

Beide Änderungen sind enger gefasst, als es auf den ersten Blick scheint. Taylors Änderung betrifft die meisten Laravel-Pakete, aber nicht das Hauptrepository laravel/framework. Fabiens Änderung betrifft genau ein Repository, ein junges Projekt, das sich noch im Beta-Stadium befindet, und die Ankündigung stellt klar, dass sich für symfony/symfony nichts ändert. Fabien nennt die Änderung ein Experiment und will Pull Requests wieder öffnen, falls es nicht funktioniert.

Gemeinsam ist den beiden die Annahme, von der sie ausgehen. Taylor empfiehlt, bei einem Bug das Problem einem Coding Agenten zu beschreiben und einen Pull Request zu öffnen, auch wenn der Code nicht gut ist, denn „code can be iterated on“. Fabien schreibt, dass ein Agent die Implementierung oft schnell erzeugen kann, und: „Understanding the problem is harder.“ Nach Fabiens Auffassung kennt nur die meldende Person die Anwendung, die Konfiguration und die Aktion, die den Fehler ausgelöst hat. Diesen Kontext können Maintainer nicht generieren.

Beide Aussagen beruhen auf der Annahme, dass es billig geworden ist, einen Diff zu erzeugen. Sie unterscheiden sich darin, welches der beiden GitHub-Artefakte übrig bleibt, und aus dieser Entscheidung folgt alles Weitere:

Laravel-Pakete Symfony Language Tools
Was übrig bleibt Der Pull Request Das Issue
Wer den Agenten einsetzt Wer beiträgt, in der eigenen Umgebung Maintainer, mit privatem Agenten-Gedächtnis und interner Testinfrastruktur
Was bei den Maintainern ankommt Ein vorgeschlagener Fix, plus die Problembeschreibung, die gerade mitkommt Das beobachtete Fehlverhalten: Versionen, Konfiguration, Logs, Screenshots
Wie geprüft wird Durch Lesen und Verstehen eines Diffs Durch Reproduzieren eines Berichts
Worauf Beitragende verzichten Einen Bug ohne Patch zu melden Code beizusteuern
Was ungebeten mitkommt Plausible, aber falsche Fixes Spekulative, massenhaft generierte Berichte

Keine der beiden Spalten ist umsonst zu haben. Fabiens Ankündigung selbst hält fest, dass massenhaft generierte, spekulative Issues nicht nützlich sind. Daniel Stenberg schreibt seit Anfang 2024 über haltlose Sicherheitsmeldungen, die mit LLMs für das Bug Bounty-Programm von curl erzeugt wurden. Und alle Maintainer, die schon einmal einen großen Pull Request von Fremden reviewt haben, wissen, was ein plausibler, aber falscher Fix kostet.

Der Diff war nie der Beitrag

Fabien schließt die Ankündigung mit einer historischen Beobachtung: E-Mail-Patches wurden von Pull Requests abgelöst, und vielleicht werden Issues in Kombination mit Coding Agenten für manche Projekte zu einer weiteren Option. Diesen Vergleich möchte ich einen Schritt weiterführen.

Der E-Mail-Patch löste ein Transportproblem. Er brachte eine Änderung von einem Computer auf einen anderen, in einer Form, die patch anwenden konnte. Der Pull Request löste ein Review-Problem. Er stellte die Änderung neben eine Diskussion, ein CI-Ergebnis und einen Merge-Button, und er machte aus „schick mir einen Patch“ einen Ablauf, den Neulinge an einem Nachmittag durchlaufen konnten.

Jeder dieser Übergänge hat verändert, worauf ein Beitrag optimiert war. Der Übergang, in dem wir jetzt stecken, optimiert auf etwas Drittes: die Kosten der Verifikation.

Solange es Stunden dauerte, einen Diff zu schreiben, hatte die Person, die ihn schrieb, ihn in der Regel verstanden, sobald er fertig war. Was einen Pull Request wertvoll machte, war das Verständnis, das beim Schreiben entstanden war, und der Diff war der Beleg für dieses Verständnis. Coding Agenten haben die Stunden abgeschafft und damit auch das Verständnis, das früher nebenbei entstand.

Das Verstehen bleibt bei den Maintainern

In Schneller als Verstehen habe ich ein Experiment von Anfang dieses Jahres beschrieben. Ein Coding Agent implementierte in 15 Minuten die ACPATH-Metrik, die in einem Paper beschrieben ist. Ich habe anschließend Stunden damit verbracht herauszufinden, ob die Implementierung korrekt ist, und konnte es nicht mit Sicherheit sagen. Der Code sah sauber aus und die Tests liefen durch, aber beides sagte mir nicht, was ich wissen musste. Ein LLM-basierter Agent kann Code in einer Domäne schneller generieren, als ein Mensch diese Domäne verstehen kann.

Übertrage diese Asymmetrie auf Taylors Ablauf. Jemand stößt auf einen Bug in einem Laravel-Paket. In der Regel kennt diese Person das Paket nicht besonders gut, sonst würde sie den Bug wahrscheinlich selbst beheben. Sie beschreibt das Symptom einem Coding Agenten, und der Agent erzeugt einen Patch, der das Symptom verschwinden lässt. Dann öffnet sie einen Pull Request. Bei den Maintainern kommt ein Diff an, den die Person, die ihn eingereicht hat, nicht verstanden hat, zu einem Problem, das niemand direkt beschrieben hat.

Das Generieren ist zu den Beitragenden gewandert. Das Verstehen ist geblieben, wo es war: bei den Maintainern, und dort kommt es jetzt in einer teureren Form an. Aus einem guten Bericht herauszufinden, was ein Bug ist, ist Diagnose. Aus einem vorgeschlagenen Fix herauszufinden, was der Bug war, ist Diagnose plus Code Review, und wer den Code eingereicht hat, kann keine Fragen dazu beantworten. Taylor schreibt, dass der Pull Request das Problem trotzdem dokumentiert. Das sehe ich anders: Ein Pull Request, den ein Agent geschrieben hat, dokumentiert die Hypothese des Agenten über das Problem.

„Code can be iterated on“ stimmt in einem Projekt mit dem Sicherheitsnetz, das ich in Alles, was wir haben beschrieben habe: statische Analyse, Tests, Property-based Testing und Mutation Testing, das prüft, ob die Tests einen falschen Fix bemerken würden. In einem solchen Projekt ist ein schlechter erster Patch billig, weil die Werkzeuge ihn ablehnen, bevor ein Mensch ihn ansehen muss. In einem Paket mit dünnerem Sicherheitsnetz übernehmen die Maintainer das Iterieren, und gefährlich ist gerade der plausible Patch, der die vorhandenen Tests besteht.

Fabiens Ablauf umgeht die Asymmetrie, weil er Generieren und Verifizieren nie trennt. Dieselbe Person erzeugt mit demselben Agenten und demselben privaten Kontext den Fix und beurteilt ihn. Nur in dieser Anordnung kann die Person, die den Code reviewt, auch sagen, warum der Code so ist, wie er ist.

Privater Kontext macht eine Person zur Pipeline

Fabiens stärkstes Argument ist der Kontext. Der Agent auf Seiten der Maintainer startet nicht mit einem frischen Checkout. Ein lokales Gedächtnis hält frühere Entscheidungen, gescheiterte Experimente, Benchmark-Ergebnisse und Rahmenbedingungen des Projekts fest. Interne Werkzeuge lassen den Language Server gegen alle privaten Symfony-Anwendungen laufen, auf denen symfony.com basiert. Kein Agent von Beitragenden hat Zugriff auf irgendetwas davon.

Daraus folgt die Arbeitsteilung. Wer beiträgt, schickt, was nur diese Person hat: die Umgebung, in der etwas fehlgeschlagen ist. Die Maintainer ergänzen, was nur sie haben: den angesammelten Kontext. Wird ein Issue ausgewählt, wird es zum Auftrag für die Implementierung. Agenten schreiben den Code, die Tests und die Dokumentation. Jede Änderung wird von Maintainern geprüft und validiert.

Ich kenne diesen Ablauf. In Mehr als Best Practices habe ich beschrieben, was aus Pair Programming mit einem Agenten wird: Ein Mensch schreibt Tests, die Verhalten spezifizieren, und der Agent generiert die Implementierung. Fabien erweitert dieses Paar nach außen, sodass ein Teil der Spezifikation von jemandem kommt, der den Code nie zu sehen bekommt.

Im selben Artikel habe ich vor strukturellen Abhängigkeiten von Systemen gewarnt, deren interne Entscheidungsprozesse undurchsichtig bleiben. Jeder Fix für Symfony Language Tools läuft jetzt durch das Agenten-Gedächtnis und die Testinfrastruktur einer einzigen Person. Das ist effizient, solange diese Person verfügbar ist, und niemand außerhalb kann den Weg von einem Bericht zu einem Release nachvollziehen. In Composer und Packagist im Supply Chain-Stresstest habe ich gefragt, wem unsere Lieferkette gehört. Diese Frage gehört auch hierher, selbst wenn die Antwort lautet: Maintainer, denen wir vertrauen.

Fabien beschränkt das Experiment auf ein junges Projekt, dessen Code von Anfang an größtenteils von Agenten geschrieben wurde. Dort gehört es hin, da stimme ich zu. Ich bin mir weniger sicher, dass es dort bleibt.

Keines der beiden Modelle bildet die nächsten Maintainer aus

Keine der beiden Ankündigungen sagt, woher die nächsten Maintainer kommen sollen. Fast alle Maintainer, die ich kenne, mich eingeschlossen, sind durch dieselbe Tür gekommen: ein Bug, der sie geärgert hat, ein kleiner Fix, ein Review, aus dem sie etwas gelernt haben, ein zweiter Fix und irgendwann Commit-Rechte. Der Pull Request war der Einstieg. Er war langsam und für beide Seiten oft frustrierend, und über ihn ging Expertise von einer Generation eines Projekts an die nächste über.

Ein Projekt, das nur Issues annimmt, schließt diese Tür. Jemand kann jahrelang hervorragende Fehlerberichte schreiben, ohne den Code je anzufassen, weil das Projekt keinen Weg dafür vorsieht. Fabien will sehen, was verloren geht, wenn Beitragende keine Pull Requests einreichen können, und will dafür die Qualität der Issues, die Zeit bis zum Fix und die Erfahrungen der Meldenden betrachten. Ich halte den Einstieg für das, was verloren geht. Keine dieser Messungen wird das zeigen, weil der Verlust erst Jahre später sichtbar wird, wenn das Projekt neue Maintainer braucht.

Ein Projekt, das nur Pull Requests annimmt, behält den Einstieg, aber wer das Verstehen einem Agenten überlassen hat, lernt dabei wenig. Das Review bringt dieser Person nichts bei, was sie zum nächsten Fix mitnehmen kann. In Mehr als Best Practices habe ich eine Analyse von Millionen GitHub-Commits zitiert: Junior-Entwicklerinnen und -Entwickler verlassen sich stärker auf KI-generierten Code als erfahrene, während die messbaren Produktivitätsgewinne fast ausschließlich den Erfahrenen zugutekommen. Taylors Modell wendet diesen Befund darauf an, wie ein Ökosystem seine Maintainer gewinnt.

Beide Modelle machen ein Projekt in diesem Jahr leichter wartbar und erschweren es, das Projekt in zehn Jahren zu übergeben. Open Source hat immer von wenigen Menschen gelebt, die eine Codebasis tief verstehen, und keines der beiden Experimente bringt mehr von ihnen hervor.

Beide Modelle brauchen einen fehlschlagenden Test

Ein Beitrag, der die tatsächlichen Anforderungen beider Maintainer erfüllt, hat vier Eigenschaften.

Er transportiert den Kontext der meldenden Person, weil Maintainer diesen Teil nicht generieren können. Er ist eindeutig, weil ein Agent alles implementiert, was die Worte zulassen. Er lässt sich durch Ausführen überprüfen, ohne dass jemand ihn zuerst verstehen muss, weil Verstehen die Ressource ist, die uns fehlt. Und er ist additiv, sodass seine Übernahme nicht stillschweigend Verhalten ändern kann, auf das sich andere verlassen.

Ein Issue in Prosa hat die erste Eigenschaft und, wenn die meldende Person sorgfältig ist, die zweite. Die anderen beiden hat es nicht: Die Maintainer müssen die Reproduktion immer noch selbst bauen. Ein Pull Request hat bestenfalls die dritte, und auch nur, wenn der Fix klein genug ist, um ihn durch Ausführen zu überprüfen. Die vierte fehlt ihm regelmäßig, und am häufigsten fehlt sie Fixes, die ein Agent geschrieben hat: Ein Agent optimiert auf eine grüne Testsuite, und der schnellste Weg dorthin ist, die rote Assertion zu ändern.

Ein Artefakt hat alle vier Eigenschaften: ein fehlschlagender Test.

<?php declare(strict_types=1);
namespace Acme\Money;

use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;

#[CoversClass(Money::class)]
final class AllocationTest extends TestCase
{
    public function testAllocatingThreeWaysDoesNotLoseACent(): void
    {
        $shares = Money::EUR(100)->allocate(1, 1, 1);

        $this->assertSame(
            100,
            $shares[0]->amount() + $shares[1]->amount() + $shares[2]->amount(),
        );
    }
}

Lies diesen Test als Fehlerbericht. Er sagt, welcher Aufruf mit welchen Argumenten welches falsche Ergebnis liefert. Für einen Agenten bleibt nichts zu interpretieren. In Mehr als Best Practices habe ich argumentiert, dass Tests die Mehrdeutigkeit einer Spezifikation beseitigen, und ein Fehlerbericht spezifiziert, was der Fix leisten muss. Die Maintainer überprüfen den Bericht, indem sie ihn ausführen, was Sekunden dauert, statt einen Diff zu verstehen, was so lange dauert, wie es eben dauert. Und der Test ist rein additiv: Er fügt eine Erwartung hinzu und ändert keine. Für diese Eigenschaft habe ich in Unveränderte Tests sind der halbe Beweis argumentiert. Sobald der Fix gemergt ist, wird derselbe Test zum Regressionstest, und der Weg vom Symptom zum Commit steht an einer Stelle.

Ein fehlschlagender Test transportiert auch den Kontext der meldenden Person, reduziert auf den Teil, auf den es ankommt. Wer den Fehler nicht in einen Test bekommt, hat den Fehler noch nicht gefunden, nur seine Umgebung. Fabien weist darauf hin, dass ein minimaler Reproducer für eine Editor-Integration schwer zu erstellen sein kann. Für eine Bibliothek ist er fast nie unpraktikabel. Für Bugs, die erst oberhalb einer einzelnen Unit auftreten, verwendet die Testsuite von PHPUnit selbst End-to-End-Tests in Form von .phpt-Dateien.

Damit verschiebt sich die Frage „Laravel oder Symfony?“. Wenn der Beitrag der fehlschlagende Test ist, dann ist es ein Detail des Ablaufs, wer ihn grün macht. Taylor kann das dem Agenten der Beitragenden überlassen und bekommt einen Pull Request, dessen erster Commit der Test ist und dessen zweiter Commit ein Fix ist, für den der Test bereits bürgt. Fabien kann das dem eigenen Agenten überlassen und bekommt ein Issue, das sich ausführen lässt. Welcher Tab abgeschaltet ist, spielt dann eine viel kleinere Rolle, weil das Projekt in beiden Fällen den Test bekommt.

PHPUnit verlangt einen verantwortlichen Menschen

Die Richtlinien für Beiträge zu PHPUnit verlangen, dass ein Mensch für jeden Beitrag verantwortlich ist. Issues und Pull Requests muss ein Mensch öffnen, und dieser Mensch darf keinen Coding-Assistenten für sich handeln oder sprechen lassen, weder beim Öffnen noch beim Antworten auf Rückfragen oder Review-Feedback. Mit einem Agenten will ich nicht kommunizieren, und mit einem Menschen, der nur weitergibt, was ein Agent schreibt, ebenso wenig.

Wer ein Issue oder einen Pull Request öffnet, erklärt damit, den Inhalt gelesen und verstanden zu haben, ihn für korrekt zu halten und ihn erklären und verteidigen zu können. Wer den gemeldeten Bug nicht reproduziert hat, sagt das ausdrücklich. Wer einen Coding-Assistenten eingesetzt hat, um ein Issue zu finden, zu reproduzieren oder aufzuschreiben, oder um Code für einen Pull Request zu generieren, legt offen, wie. Diese Offenlegung disqualifiziert einen Beitrag nicht. Sie ändert, wie wir ihn einordnen und reviewen, so wie es einen Unterschied macht, ob ein Fehlerbericht von einem minimierten Reproducer oder von einem Stack Trace aus der Produktion stammt.

Für einen Fehlerbericht verlangen die Richtlinien Schritte zur Reproduktion und Beispielcode, für einen Pull Request Tests. Was ich oben argumentiert habe, steht dort nicht, also sage ich es hier. Der beste Fehlerbericht für PHPUnit ist ein fehlschlagender Test. Einen Pull Request, der nichts als einen fehlschlagenden Test enthält, nehme ich gerne entgegen. Kommt ein Bericht als Prosa, versuche ich als Erstes, einen Test daraus zu machen. Gelingt das nicht, lernen wir aus dem gescheiterten Versuch meist mehr als aus dem Bericht.

Bei Bugfixes prüfe ich zuerst das additive Kriterium. Ändert ein Fix eine bestehende Erwartung, beginnt das Review mit der Frage nach dem Warum, und es gelten die drei Diagnosen aus Unveränderte Tests sind der halbe Beweis. Ob ein Agent den Fix getippt hat, ändert an diesem Kriterium nichts. Von der Person, die den Pull Request öffnet, will ich wissen, von welchem fehlschlagenden Test sie ausgegangen ist.

Beitragsmodelle werden der Form eines Projekts folgen

Beide Experimente sind wenige Wochen alt, und beide lassen sich mit einer Einstellung im Repository rückgängig machen. Was folgt, sind Prognosen.

Taylor vermutet, dass die meisten Open Source-Bibliotheken bald so arbeiten werden wie jetzt die Laravel-Pakete. Ich erwarte stattdessen, dass sich Beitragsmodelle nach der Form eines Projekts aufteilen. Issue-first passt zu einem jungen Projekt, dessen Code größtenteils Agenten geschrieben haben und in dem der private Kontext einer einzelnen Person dominiert. PR-first passt zu einem Ökosystem aus vielen Paketen und Integrationen, in dem ein Fehler oft nur in der Umgebung der meldenden Person auftritt und der Agent der Beitragenden wenigstens dort laufen kann. Die meisten gereiften Projekte werden keins von beiden tun und stattdessen die Regeln schärfen, die sie schon haben. Ich würde keines der beiden Modelle übernehmen, nur weil ein bekanntes Projekt es ausprobiert.

Die Urheberschaft wird zu einer offenen Frage. In den Kommentaren unter Fabiens Ankündigung hat Alexander Schranz vorgeschlagen, die Autorinnen und Autoren eines Issues und alle, die es mit wertvollen Kommentaren bereichert haben, im Merge-Commit per Co-authored-by-Trailer als Co-Autorinnen und Co-Autoren aufzuführen. Ich halte das für eine vernünftige Antwort. Sonst zeigt die Beitragshistorie eines Projekts bald nicht mehr, wer was verstanden hat, und Projekte müssen bewusst entscheiden, was ein Eintrag in dieser Historie bedeutet, wenn der Diff generiert wurde.

Und für Beitragende wird wieder die Fähigkeit zählen, die schon auf den Mailinglisten zählte, bevor Pull Requests sie optional gemacht haben: einen Fehler isolieren, ihn auf das Wesentliche reduzieren und so formulieren, dass jemand anderes ihn ausführen kann. Auf den Mailinglisten hieß das Reproducer. In einem PHP-Projekt heißt es heute fehlschlagender Test.