Blog

  • Perpetuum mobile: Wie Copilot Copilot startet, um ganze Roadmaps abzuarbeiten

    Ich bin als diplomierter Informatiker vor allem eins: faul.

    Das klingt jetzt vielleicht erstmal nicht nach dem Satz, mit dem man in ein Bewerbungsgespräch gehen sollte, aber ich halte das im technischen Kontext für eine ganz hervorragende Eigenschaft. Nicht diese “ich lasse alles liegen und hoffe, dass jemand anders den Müll rausbringt”-Faulheit. Eher diese andere. Die nützliche. Die, bei der man sich denkt: Wenn ich das dreimal machen muss, kann ich auch gleich zwei Stunden investieren, damit ich es nie wieder machen muss.

    Nun.

    Ganz so einfach ist es natürlich nie.

    Ich mag es nämlich nicht nur bequem, ich mag auch, wenn Dinge gut laufen. Und das ist eine ungünstige Kombination, weil “gut laufen” meistens bedeutet, dass man sich vorher ein paar Gedanken machen muss. Man kann Dinge natürlich auch einfach irgendwie zusammenklicken, bis sie zufällig funktionieren, aber dann hat man später diese schöne Situation, in der man um 23:47 Uhr auf ein Dashboard starrt und flüstert: “Warum bist Du so?”

    Programmieren konnte ich dabei nie besonders gut. Jedenfalls nicht in dem Sinne, in dem Menschen programmieren können, die am Wochenende aus Versehen einen Compiler schreiben, weil ihnen beim Kaffee langweilig war. Ich war immer eher der Typ: Ich weiß ziemlich genau, wie die Software sich verhalten sollte, ich weiß auch, warum die vorhandene Software das natürlich wieder nicht tut, und ich habe dann eine sehr ausgeprägte Meinung dazu, wie eine kleine Individualsoftware aussehen müsste, die das Problem elegant aus der Welt schafft.

    Das Problem daran: Irgendwer muss diese Individualsoftware dann bauen.

    Früher war dieser Irgendwer oft ich. Mit unterschiedlichem Erfolg. Es gab funktionierende Dinge, es gab Dinge, die eher Charakter hatten, und es gab Dinge, die man nachträglich nur mit “war halt 2004” entschuldigen kann.

    Heute gibt es Copilot, Codex und die ganze Familie der fleißigen Textwahrscheinlichkeitsmaschinen. Und damit entsteht eine interessante neue Form der Faulheit: Man schreibt nicht mehr zwingend jeden Code selbst, sondern man beschreibt ziemlich genau, was als nächstes passieren soll, lässt einen Agenten loslaufen, schaut sich das Ergebnis an, meckert bei Bedarf und merged, wenn es tut.

    Neudeutsch heißt das dann gerne Vibe Coding. Man hat eine Idee, redet ein bisschen mit ChatGPT darüber, bekommt eine Roadmap, lässt sich Issues daraus schneiden und fühlt sich für ungefähr sieben Minuten wie ein sehr effizienter Produktmanager mit eigenem Maschinenraum.

    Das ist auch gar nicht falsch. Es ist sogar ziemlich gut.

    Nur kommt danach der langweilige Teil.

    Denn wenn die Roadmap einmal da ist, liegen da plötzlich zwanzig, dreißig oder fünfzig Issues herum. Alle sehen irgendwie sinnvoll aus. Viele davon stammen ohnehin aus demselben ChatGPT-Gespräch. Und jetzt soll ich als Mensch ernsthaft stundenlang Tickets sortieren, Copilot zuweisen, warten, gucken, nächstes Ticket suchen, wieder zuweisen, wieder warten?

    Das ist natürlich Quatsch.

    Also macht man, was man als fauler Informatiker macht: Man automatisiert den langweiligen Teil.

    So weit, so nett. Aber nach dem ersten erfolgreichen Copilot-PR kommt natürlich sofort die nächste Frage:

    Warum muss ich eigentlich den nächsten Copilot-Lauf wieder selbst anschubsen?

    Und direkt danach kommt die zweite Frage, die meistens etwas mehr nach verbranntem Toast riecht:

    Warum habe ich eigentlich gerade acht Copilot-PRs gleichzeitig, die alle dieselbe Datei anfassen?

    Und damit sind wir beim Perpetuum mobile.

    Die Idee

    Ein echtes Perpetuum mobile funktioniert nicht. Thermodynamik, Physikunterricht, sehr traurige Bastler mit Magneträdern, ihr kennt das.

    Ein Repository kann aber so tun als ob.

    Die Idee ist simpel: Wenn ein Projekt eine halbwegs vernünftige Roadmap hat, dann kann man diese Roadmap in Issues schneiden. Und wenn diese Issues gut genug beschrieben sind, dann kann Copilot sie nacheinander abarbeiten. Und wenn ein Issue fertig ist, kann ein Workflow das nächste Issue aktivieren. Und wenn das fertig ist, das nächste. Und so weiter, bis entweder die Roadmap leer ist oder die Realität wieder eine Produktentscheidung verlangt.

    Das ist kein “KI übernimmt jetzt alles”-Zauber. Das ist eher ein Förderband. Man legt vorne sinnvoll portionierte Arbeit drauf, und hinten kommen Pull Requests raus. Manchmal gute. Manchmal welche, bei denen man dem Agenten freundlich erklärt, dass “funktioniert bei mir” kein Testkonzept ist.

    Der entscheidende Punkt ist dabei das nacheinander.

    Copilot kann parallel arbeiten. GitHub kann parallel Issues zuweisen. Moderne Agenten können mit beeindruckender Geschwindigkeit beeindruckend viele Branches erzeugen. Das klingt erstmal nach Produktivität, ist aber bei echten Projekten sehr schnell nur eine andere Schreibweise für Merge-Konflikte.

    Issue 4 ändert die API. Issue 5 baut schon gegen die alte API. Issue 6 räumt nebenbei Komponenten auf. Issue 7 hat dieselbe Datei “nur kurz” formatiert. Und dann sitzt man da, betrachtet vier Pull Requests, die einzeln plausibel sind, zusammen aber aussehen wie ein Auffahrunfall auf der A3 bei Regen.

    Genau diesen Unsinn will ich nicht.

    Ich will nicht keine Kontrolle. Ich will nur nicht die Art Kontrolle, bei der ich den ganzen Tag Tickets von links nach rechts schiebe, obwohl die Reihenfolge vorher schon in der Roadmap steht.

    Die Zutaten

    Man braucht dafür erstaunlich wenig:

    • eine Roadmap, die nicht nur aus Wunschdenken und Nebel besteht,
    • GitHub Issues mit klaren Akzeptanzkriterien,
    • ein paar Labels,
    • einen Sequencer, der das nächste Issue aktiviert,
    • und optional einen Automerge-Workflow, der nur dann merged, wenn die Welt nicht brennt.

    Die Labels sind dabei die eigentliche Fernbedienung:

    copilot:queued   liegt bereit und darf irgendwann drankommen
    copilot:active   ist gerade an der Reihe
    agentic-ready    ist klein genug und ordentlich beschrieben
    blocked          bitte nicht anfassen, hier fehlt noch Hirn
    priority:high    kommt weiter nach vorne

    Das Schöne daran ist, dass es nicht besonders schlau ist. Und das meine ich positiv. Sobald Automatisierung zu schlau sein möchte, entwickelt sie gerne eine Persönlichkeit, die ich in Produktionssystemen nicht haben will.

    Der Sequencer muss nicht “verstehen”, was Produktstrategie ist. Er muss nur respektieren, dass Issue 12 vor Issue 13 kommt, dass blocked wirklich blocked heißt und dass immer nur ein Copilot-Ticket aktiv sein soll. Das ist ungefähr die Menge an Intelligenz, die ich in diesem Teil des Systems haben möchte.

    Das wichtigste Teil: gute Issues

    Der Trick ist nicht der Workflow. Der Trick ist das Issue.

    Ein schlechtes Issue sagt:

    Mach mal Login besser.

    Das ist kein Issue. Das ist ein Hilferuf.

    Ein brauchbares Issue sagt eher:

    ## Ziel
    Die Setup-Seite zeigt fehlende Mailbox-Recovery-Konfigurationen an.
    
    ## Akzeptanzkriterien
    - Fehlende Werte erscheinen als Warnung.
    - Die Warnung nennt den konkreten nächsten Schritt.
    - Der Health-Endpoint liefert dieselbe Information maschinenlesbar.
    - Tests decken fehlende und vorhandene Konfiguration ab.
    
    ## Nicht-Ziele
    - Keine neue Provider-Integration.
    - Keine Änderung an bestehenden Secrets.

    Damit kann ein Agent arbeiten. Da steht drin, was passieren soll, woran man erkennt, dass es fertig ist, und wo er bitte seine Finger rauslassen soll. Letzteres ist wichtig, weil Agenten sonst manchmal den Ehrgeiz eines Praktikanten am dritten Tag entwickeln: “Ich habe nebenbei noch das Auth-System refactored.”

    Danke. Nein.

    Der Sequencer

    Der Sequencer ist ein kleines Script, das regelmäßig schaut, ob gerade ein Issue aktiv ist. Falls nein, sucht es das nächste offene, nicht blockierte Issue mit copilot:queued und macht daraus copilot:active.

    Praktisch bedeutet das: Ich kann eine von ChatGPT vorbereitete Roadmap nehmen, daraus GitHub Issues erzeugen, die Issues einmal sauber priorisieren und dann dem Repository sagen: Hier, arbeite das bitte der Reihe nach ab. Nicht alles auf einmal. Nicht “mal schauen, was passiert”. Sondern schön eins nach dem anderen, wie bei Menschen, nur ohne Stand-up.

    Das kann man sehr schlicht per GitHub Actions laufen lassen:

    name: Copilot Issue Sequencer
    
    on:
      schedule:
        - cron: "17 * * * *"
      workflow_dispatch:
    
    jobs:
      activate-next:
        runs-on: ubuntu-latest
        permissions:
          issues: write
          contents: read
        steps:
          - uses: actions/checkout@v4
          - uses: actions/setup-node@v4
            with:
              node-version: "22"
          - run: node scripts/copilot-issue-sequencer.mjs

    Warum nicht alles gleichzeitig aktivieren?

    Weil das dann keine Automation ist, sondern Konfetti mit Schreibrechten.

    Ein aktives Issue nach dem anderen ist langweilig, aber langweilig ist bei Infrastruktur eine unterschätzte Tugend.

    Natürlich ist das langsamer als zehn Agenten gleichzeitig losrennen zu lassen. Aber es ist schneller als zehn Agenten gleichzeitig losrennen zu lassen und danach einen Nachmittag damit zu verbringen, aus fünf halb überlappenden PRs wieder eine lineare Geschichte zu basteln.

    Automerge, aber nicht lebensmüde

    Der zweite Teil ist Automerge. Auch hier gilt: Man kann das vernünftig machen oder man kann sich später wundern.

    Meine Mindestregeln wären:

    • nur Copilot-PRs oder explizit markierte PRs,
    • keine Drafts,
    • alle Pflichtchecks grün,
    • keine blockierten Issues,
    • riskante Bereiche brauchen weiter menschliches Review.

    Damit ist Automerge nicht “Augen zu und durch”, sondern eher: Wenn alles, was wir vorher als wichtig definiert haben, grün ist, dann bitte nicht noch drei Tage auf einen Klick warten.

    Und genau hier schließt sich der Kreis: Wenn ein PR sauber merged, kann der nächste Lauf starten. Nicht, weil irgendein Mensch gerade zufällig vor GitHub sitzt, sondern weil das Projekt selbst weiß: Dieses Stück ist erledigt, jetzt kommt das nächste.

    Roadmap schneiden

    Der Teil, der gerne unterschätzt wird: Man muss die Roadmap vorher ehrlich machen.

    “Wir bauen eine großartige Plattform” ist keine Roadmap. “Wir bauen zuerst Import/Export, dann persistente Sessions, dann ein Rechtekonzept, dann Auswertungen” ist schon besser. “Issue 12 baut den Storage-Adapter, Issue 13 hängt Quiz-Inhalte daran, Issue 14 exportiert Ergebnisse als CSV und JSON” ist agententauglich.

    Das ist übrigens auch der Moment, an dem ChatGPT wieder sehr nützlich ist. Nicht als Orakel, sondern als Roadmap-Zerlegehilfe. Man kann sagen: Hier ist das Projekt, hier sind die Ziele, mach daraus bitte Arbeitspakete, die ein Agent in einem überschaubaren Pull Request erledigen kann. Danach muss man trotzdem noch drüberschauen. Aber das ist eine andere Art Arbeit als stumpfes Ticket-Schubsen.

    Man merkt dabei auch sehr schnell, welche Dinge noch nicht reif sind. Und das ist gut. Dann kommt blocked drauf. Nicht als Schande, sondern als kleines gelbes Warnschild: Hier bitte erst denken, dann Agent.

    Fazit

    Am Ende ist das Ganze weniger Science-Fiction, als es klingt. Es ist ein Issue-Stapel mit Anstand, ein kleines Script, ein paar Labels und die Bereitschaft, Arbeit so zu beschreiben, dass nicht nur ein Mensch mit viel Kontext sie versteht.

    Für jemanden wie mich ist das ziemlich nah an idealer Faulheit: Ich muss immer noch denken, aber ich muss nicht mehr jeden Kleinkram selbst heruntertippen. Ich muss immer noch kontrollieren, aber ich muss nicht mehr jedes Ticket einzeln anschieben. Ich muss immer noch wissen, was ich will, aber ich darf den langweiligen Teil häufiger an Maschinen delegieren.

    Und wenn Copilot dann einen PR baut, der merged, woraufhin der Workflow das nächste Issue aktiviert, woraufhin Copilot wieder losläuft, dann ist das natürlich kein echtes Perpetuum mobile.

    Aber es fühlt sich kurz so an.

    Und für einen faulen Informatiker ist das schon ziemlich nah dran.

  • Wie man gute Pubquizfragen baut – oder: Im Zweifel Kanada

    Wie man gute Pubquizfragen baut – oder: Im Zweifel Kanada

    Ein gutes Pubquiz zu bauen ist deutlich schwieriger, als es auf den ersten Blick aussieht.

    Das klingt erstmal komisch, weil Fragen stellen ja nun wirklich jeder kann. “Was ist die Hauptstadt von Frankreich?”, “Wer sang Bohemian Rhapsody?”, “Wie viele Beine hat eine Spinne?” und fertig ist die Laube.

    Nun.

    Leider nein.

    Ein Pubquiz ist kein Einstellungstest, keine Abiturprüfung, kein Kneipen-Trivial-Pursuit und auch kein Versuch, den Raum davon zu überzeugen, dass man selbst im Wikipedia-Kaninchenbau besonders tief graben kann. Ein gutes Pubquiz ist im Idealfall ein Abend, an dem sehr unterschiedliche Menschen gemeinsam rätseln, diskutieren, sich kurz sicher sind, dann wieder unsicher werden, am Ende doch die erste Idee nehmen und anschließend entweder triumphieren oder leise fluchen.

    Und genau da wird es schwierig.

    Denn ein Quiz muss balanciert sein.

    Bei einem Quiz mit 50 Punkten sollte genug Diversifizierung drin sein, damit nicht nach Runde zwei klar ist, welches Team gewinnt. Es braucht Popkultur, Geschichte, Geographie, Musik, Sport, Wissenschaft, Alltag, vielleicht ein bisschen Lokalkolorit und idealerweise ein paar Fragen, bei denen man nicht einfach Wissen abruft, sondern gemeinsam herleitet.

    Gleichzeitig darf man Einsteiger nicht direkt verlieren.

    Wenn die ersten fünf Fragen so gebaut sind, dass nur Menschen mit einem Nebenfach in byzantinischer Münzprägung überhaupt eine Chance haben, dann ist das vielleicht intellektuell befriedigend für den Fragenersteller, aber kein guter Abend für den Raum.

    Die Kunst liegt darin, Fragen zu bauen, bei denen Teams zumindest das Gefühl haben, sie könnten drauf kommen.

    Das ist ein großer Unterschied.

    Eine gute Frage ist nicht zwingend leicht. Eine gute Frage gibt aber Anknüpfungspunkte. Man erkennt vielleicht ein Jahrzehnt, ein Land, eine Wortherkunft, eine Melodie, einen Schauspieler, ein Logo, einen Zusammenhang. Dann beginnt dieses schöne Pubquiz-Gemurmel am Tisch:

    “Warte, das war doch Kanada, oder?”
    “Nein, Kanada war das andere.”
    “Bist Du sicher?”
    “Nein.”
    “Dann nehmen wir Kanada.”

    Und manchmal ist das sogar richtig.

    Im Zweifel ist die Antwort nämlich Kanada.

    Nicht immer natürlich. Aber erstaunlich oft genug, dass man daraus eine Regel machen möchte.

    Was man bei Quizfragen vermeiden sollte: reine Binär-Abfragen ohne Spaß. “In welchem Jahr wurde exakt X gegründet?” ist meistens keine gute Frage, wenn das Jahr nicht irgendwie herleitbar oder kulturell verankert ist. 1871? 1949? 1969? Alles kann alles sein. Das ist dann kein Rätseln, sondern Datums-Lotto.

    Besser ist: eine Frage so bauen, dass die Antwort zwar konkret ist, der Weg dahin aber über gemeinsames Denken führt.

    Also nicht:

    “In welchem Jahr wurde Kanada gegründet?”

    Sondern eher:

    “Welches heutige G7-Land entstand 1867 durch den British North America Act als Dominion?”

    Das ist immer noch nicht geschenkt. Aber es hat Haken. G7. British. North America. Dominion. Man kann arbeiten.

    Ein anderes Problem: Fragen, die nur für eine bestimmte Altersgruppe funktionieren. Wenn jede Musikfrage aus den 90ern kommt, freuen sich Menschen meines Alters, aber der Rest schaut irgendwann so, wie ich schaue, wenn jemand aktuelle TikTok-Sounds abfragt. Umgekehrt funktioniert es natürlich genauso.

    Ein gutes Quiz braucht daher Streuung.

    Nicht nur thematisch, sondern auch generationell. Eine 20-jährige Person sollte Punkte holen können. Eine 50-jährige Person auch. Das Team mit Sportnerd soll einen Moment haben, das Team mit Literaturmensch ebenfalls. Und irgendwo muss es eine Frage geben, bei der alle am Tisch “ach komm, das weiß doch jeder” sagen und dann drei verschiedene Antworten aufschreiben.

    Wichtig ist auch die Dramaturgie.

    Man sollte nicht mit den härtesten Fragen starten. Die ersten Punkte müssen einladend sein. Teams sollen reinkommen, sich sortieren, erste Erfolgserlebnisse haben. Danach darf es schwerer werden. Ein Quizabend verträgt ein paar richtige Brecher, aber nicht 50 davon.

    Ich mag Fragen, die auf den ersten Blick leicht wirken und dann eine kleine Drehung haben. Oder solche, bei denen die intuitive Antwort tatsächlich die richtige ist.

    Das ist überhaupt eine der schönsten Pubquiz-Beobachtungen: Die erste Antwort ist oft die beste.

    Nicht immer. Aber oft.

    Teams reden sich regelmäßig aus richtigen Antworten heraus. Da steht nach zehn Sekunden das Richtige auf dem Zettel, dann kommen Zweifel, dann kommt jemand mit gefährlichem Halbwissen, dann wird umgeschrieben, und am Ende ist es falsch.

    Das gehört dazu. Das ist sogar Teil des Spiels.

    Ein guter Quizmaster baut Fragen so, dass dieser Moment entstehen kann, ohne unfair zu sein. Nicht durch Trickserei, nicht durch “haha, reingelegt”, sondern durch saubere Formulierung und plausible Distraktoren im Kopf der Teams.

    Am Ende ist ein gutes Pubquiz kein Wissenstest, sondern ein sozialer Mechanismus mit Bierbegleitung.

    Es muss genug einfache Punkte geben, damit neue Teams wiederkommen.

    Es muss genug schwere Punkte geben, damit gute Teams sich anstrengen müssen.

    Es muss genug Vielfalt geben, damit niemand den ganzen Abend nur Deko ist.

    Und es muss mindestens eine Frage geben, bei der man später sagt:

    “Verdammt. Ich wollte Kanada nehmen.”

    Heiter weiter!

    C.

  • It’s been a while. Again.

    Latest News Subscribe Update

    Nach gefühlten drölf Jahren gibt es mal wieder neuen Content auf diesem Internettagebuch.

    Warum?

    Nun.

    Unter anderem, weil der Küchenserver noch da ist. Was an sich schon bemerkenswert ist, denn Dinge im Internet haben ja inzwischen oft eine Halbwertszeit irgendwo zwischen “Startup wurde acqui-hired” und “wir haben unsere API leider eingestellt”.

    Dieses Blog hingegen steht noch. Es hat Umzüge, PHP-Versionen, WordPress-Updates, kaputte Plugins, Themewechsel und diverse Phasen meines Lebens überlebt. Also eigentlich alles, was man einem privaten Blog so zumuten kann.

    Und vielleicht ist genau das gerade wieder charmant.

    Nicht jeder Gedanke muss in eine Timeline, nicht jede Beobachtung braucht einen Algorithmus, nicht jeder kleine technische Fund muss in irgendeinem Slack verschwinden, wo ihn drei Tage später niemand mehr findet. Manchmal reicht auch ein Blogpost. Eine URL. Ein Datum. Fertig.

    Ob das hier jetzt wieder regelmäßig passiert?

    Keine Ahnung.

    Ich nehme mir ausdrücklich nicht vor, ab jetzt wieder wöchentlich zu bloggen, weil solche Vorsätze ungefähr so zuverlässig sind wie Drucker unter Windows, Faxsoftware im Jahr 2020 oder “nur mal eben” ein Docker-Compose-File anzupassen.

    Aber ein paar Dinge gäbe es schon aufzuschreiben.

    Über Technik, die still funktionieren darf. Über Selfhosting und die Frage, ab wann Hobby in Infrastruktur übergeht. Über Backups, bei denen Restore der einzige Teil ist, der zählt. Über Reisen, Hamburg, Alltag, Automatisierung, vielleicht auch darüber, warum gute Pubquizfragen schwieriger sind als schlechte Architekturdiagramme.

    Also: mal sehen.

    Der Küchenserver steht noch.

    Heiter weiter!

    C.

  • nginx zur verzögerten Auslieferung von Webhinhalten nutzen

    nginx zur verzögerten Auslieferung von Webhinhalten nutzen

    Oder: Wie ich lernte nginx zu lieben.

    Irgendwie will doch jedermann, dass Webseiten schnell laden. Wir investieren viel Zeit und Energie darin, dies möglich zu machen.
    Webseiten, oder einzelne Dateien, bewusst in der Auslieferung zu verzögern hingegen, das ist etwas, was ab und an nützlich ist, aber ein 08/15 Anwendungsfall.
    Entsprechend ist es auch nicht so einfach, das “how to” dafür zu finden: Ich ging geistig schon meine PHP Kenntnisse durch und plante, Inhalte per PHP von Platte zu lesen, um sie dann nach einem delay im PHP-Skript verzögert ausliefern zu lassen.
    Dank nginx geht’s jedoch auch einfach. Nginx, genauer das echo Modul, unterstützt von Haus aus die Verzögerung.
    Folgende Konfiguration hat hier sehr geholfen:

    # Static files
    location /delay {
        echo_sleep 5;
        echo_exec @default;
    }
    location @default {}

    Hier werden Inhalte im Unterverzeichnis erst nach fünf Sekunden ausgeliefert.
    Gerne klauen und bei Bedarf anpassen!