<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Werkstatt-Log — DoMe Dynamics</title>
  <subtitle>Notizen, Lernsignale und Reflexionen aus dem laufenden Bau agentischer Systeme.</subtitle>
  <link href="https://dynamic-dome.com/feed.xml" rel="self" type="application/atom+xml"/>
  <link href="https://dynamic-dome.com/log/" rel="alternate" type="text/html"/>
  <id>https://dynamic-dome.com/log/</id>
  <updated>2026-07-03T00:00:00Z</updated>
  <author><name>Dominic Meiser</name></author>
  <entry>
    <title>Kampagnen-Report: 36 Audit-Befunde, 6 Agenten, eine Nacht</title>
    <link href="https://dynamic-dome.com/log/kampagnen-report-6x6-dispatch/" rel="alternate" type="text/html"/>
    <id>https://dynamic-dome.com/log/kampagnen-report-6x6-dispatch/</id>
    <published>2026-07-03T00:00:00Z</published>
    <updated>2026-07-03T00:00:00Z</updated>
    <summary>Wie eine 6×6-Agenten-Kampagne die mechanischen Befunde eines Website-Audits abarbeitete: Worktree-Isolation, Branch pro Kampagne, Review vor jedem Merge — und ein Plan-B-Schwenk, als die Codex-Sandbox unter Windows nicht mitspielte.</summary>
    <category term="agentic"/>
    <category term="orchestrierung"/>
    <category term="kampagnen-report"/>
    <category term="claude-code"/>
    <category term="worktrees"/>
    <content type="html">&lt;p&gt;Der Codex-Audit-Scheduler dieser Website hatte über Wochen Findings angesammelt. Nach einem Reconcile-Lauf blieben &lt;strong&gt;36 echt offene, rein mechanische Befunde&lt;/strong&gt; übrig — Link-Labels, Datums-Konsistenz, Mobile-Feinschliff, ein Sanitizer-Leck. Nichts davon braucht eine Design-Entscheidung. Alles davon kostet einzeln abgearbeitet einen Nachmittag.&lt;/p&gt;
&lt;p&gt;Also: nicht einzeln abarbeiten. Dieser Report dokumentiert die erste Kampagnen-Nacht — als Format, das ab jetzt pro Großaktion wiederholt wird.&lt;/p&gt;
&lt;h2&gt;Der Zuschnitt: 6 Cluster, ehrlich gezählt&lt;/h2&gt;
&lt;p&gt;Die 36 Befunde wurden nach Themen geclustert, nicht nach Dateien: DCO-Showcase-Pflege, Link-Label-Konsistenz, Freshness-Signale, Séance-Sanitizer + OG-Pipeline, Mobile/A11y, technischer Sweep. Ein Befund war zu trivial für einen eigenen Cluster und wurde direkt gefixt — übrig blieben 35 in sechs Clustern im Format 6/6/6/6/6/5.&lt;/p&gt;
&lt;p&gt;Das klingt pedantisch, ist aber Absicht: &lt;strong&gt;kein Auffüllen mit stale Items, um die runde Zahl voll zu machen.&lt;/strong&gt; Eine Kampagne, die ihre eigene Bilanz schönt, ist als Beweisformat wertlos.&lt;/p&gt;
&lt;h2&gt;Der Plan-B-Schwenk&lt;/h2&gt;
&lt;p&gt;Der ursprüngliche Dispatch-Plan sah sechs Codex-Subagenten vor. Der erste Lauf scheiterte an der Codex-Sandbox unter Windows — Schreibzugriffe ins Temp-Verzeichnis, ein bekanntes, dokumentiertes Problem. Statt die Sandbox aufzubohren, wechselte die Kampagne das Werkzeug: &lt;strong&gt;sechs Claude-Subagenten, jeder in einem eigenen Git-Worktree, jeder auf einem eigenen Branch&lt;/strong&gt; (&lt;code&gt;codex/campaign-c11&lt;/code&gt; bis &lt;code&gt;c16&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;Die Lehre daraus ist wichtiger als der Umweg: Die Kampagnen-Mechanik — Cluster, Isolation, Branch, Review, Merge — ist werkzeugneutral. Welches Modell die Arbeit macht, ist eine Austauschentscheidung, kein Architekturbruch.&lt;/p&gt;
&lt;h2&gt;Die Leitplanken&lt;/h2&gt;
&lt;p&gt;Jeder Agent bekam dasselbe Protokoll als Pflichtlektüre:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Branch-only&lt;/strong&gt;: kein Commit auf main, kein Push, kein Deploy.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Worktree-Isolation&lt;/strong&gt;: sechs Agenten parallel, null gegenseitige Konflikte.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Nur grüne Befunde&lt;/strong&gt;: was eine Owner-Entscheidung braucht, wird nicht &quot;mitgenommen&quot;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Chirurgisch stagen&lt;/strong&gt;: fremde Drift bleibt unangetastet.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Der letzte Punkt hat einen eigenen Auslauf: Strittiges landete nicht still im Diff, sondern in einer &lt;strong&gt;NEEDS-DOMINIC-Liste&lt;/strong&gt; — vier Entscheidungsvorlagen, die der Owner am nächsten Morgen einzeln entschied (und die dann als eigener, nachvollziehbarer Commit umgesetzt wurden). Ein Agent, der bei Unsicherheit parkt statt rät, ist der Unterschied zwischen Delegation und Kontrollverlust.&lt;/p&gt;
&lt;h2&gt;Die Bilanz&lt;/h2&gt;
&lt;p&gt;Nach den sechs Läufen kam &lt;strong&gt;eine Review-Runde über alle sechs Branches&lt;/strong&gt; (Standards + Spec), dann wurden die Kampagnen einzeln gemergt — sechs Merge-Commits (&lt;code&gt;03d5391&lt;/code&gt; bis &lt;code&gt;848d8ff&lt;/code&gt;; das Site-Repo ist privat, ein Walkthrough durch den Log ist auf Anfrage möglich):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;6 Kampagnen-Branches, alle in einer Nacht gemergt&lt;/li&gt;
&lt;li&gt;43 geänderte Dateien, ~1.215 eingefügte Zeilen&lt;/li&gt;
&lt;li&gt;4 geparkte Entscheidungsvorlagen statt 4 stiller Annahmen&lt;/li&gt;
&lt;li&gt;Testsuite nach dem letzten Merge: komplett grün&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Was die Kampagne bewusst &lt;strong&gt;nicht&lt;/strong&gt; behauptet: dass 36/36 gefixt wurden. Einzelne Befunde erwiesen sich beim Ground-Truthing als bereits erledigt oder stale — auch das steht im Run-Report. Die ehrliche Zählung ist Teil des Formats.&lt;/p&gt;
&lt;h2&gt;Warum das ein Format ist, kein Einzelfall&lt;/h2&gt;
&lt;p&gt;Der eigentliche Ertrag ist nicht die abgearbeitete Liste, sondern die wiederholbare Mechanik: &lt;strong&gt;Backlog clustern → Flotte dispatchen → isoliert bauen lassen → zentral reviewen → einzeln mergen → Strittiges eskalieren.&lt;/strong&gt; Dieselbe Struktur trug wenige Tage vorher schon den Bau des &lt;a href=&quot;/observatory&quot;&gt;Agent-Observatory&lt;/a&gt; mit 15 Agenten in einem Durchlauf.&lt;/p&gt;
&lt;p&gt;Jede künftige Großaktion an dieser Website bekommt einen solchen Report. Nicht als Erfolgsmeldung — als Prüfprotokoll.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Wie viel Parallelität verträgt ein Repo, bevor Review zum Flaschenhals wird?&lt;/strong&gt; Wenn dich die Dispatch-Mechanik im Detail interessiert, &lt;a href=&quot;/?anlass=feedback#kontakt&quot;&gt;schreib mir&lt;/a&gt;.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>So arbeite ich: eine Agenten-Flotte, ein Review, ein Merge</title>
    <link href="https://dynamic-dome.com/log/so-arbeite-ich-flotten-dispatch/" rel="alternate" type="text/html"/>
    <id>https://dynamic-dome.com/log/so-arbeite-ich-flotten-dispatch/</id>
    <published>2026-07-03T00:00:00Z</published>
    <updated>2026-07-03T00:00:00Z</updated>
    <summary>Meta-Case-Study der Arbeitsform hinter dieser Website: Backlog clustern, Agenten-Flotte in Worktrees dispatchen, adversarial reviewen, einzeln mergen. Mit zwei echten Nächten als Beleg — der 6×6-Kampagne und dem 15-Agenten-Observatory-Build.</summary>
    <category term="agentic"/>
    <category term="orchestrierung"/>
    <category term="arbeitsweise"/>
    <category term="multi-agent"/>
    <category term="review"/>
    <content type="html">&lt;p&gt;Die einzelnen Case-Studys dieser Website zeigen &lt;em&gt;Systeme&lt;/em&gt;. Dieser Text zeigt die &lt;em&gt;Arbeitsform&lt;/em&gt;, mit der sie entstehen — denn die ist inzwischen selbst das interessanteste Artefakt: Ich arbeite nicht mit einem Assistenten, sondern mit einer Flotte. Und die Flotte darf nichts mergen, was nicht geprüft wurde.&lt;/p&gt;
&lt;h2&gt;Das Muster in einer Zeile&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Backlog clustern → Flotte dispatchen → isoliert bauen lassen → adversarial reviewen → einzeln mergen → Strittiges eskalieren.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Als Diagramm, am Beispiel einer Kampagnen-Nacht:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;                    Backlog (36 Befunde, ground-getruthed)
                                   │
                          Cluster-Zuschnitt (6 Themen)
                                   │
        ┌─────────┬─────────┬─────┴────┬─────────┬─────────┐
        ▼         ▼         ▼          ▼         ▼         ▼
     Agent 1   Agent 2   Agent 3    Agent 4   Agent 5   Agent 6
    worktree  worktree  worktree   worktree  worktree  worktree
    branch    branch    branch     branch    branch    branch
        │         │         │          │         │         │
        └─────────┴─────────┴────┬─────┴─────────┴─────────┘
                                 ▼
                  EIN Review über alle Branches
                  (Standards-Achse + Spec-Achse)
                                 │
                 ┌───────────────┴───────────────┐
                 ▼                               ▼
        6 einzelne Merges                NEEDS-DOMINIC-Liste
        (kein Auto-Merge)                (4 Entscheidungsvorlagen,
                                          Owner entscheidet morgens)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Drei Eigenschaften machen das Muster tragfähig:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Isolation ist nicht optional.&lt;/strong&gt; Jeder Agent arbeitet in einem eigenen Git-Worktree auf einem eigenen Branch. Sechs Agenten parallel, null Merge-Konflikte untereinander — und ein missratener Lauf wird verworfen, statt das Repo zu verschmutzen.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Review ist eine eigene Instanz, nicht ein Nachsatz.&lt;/strong&gt; Wer baut, urteilt nicht über sich selbst. Das Review läuft zentral über alle Branches, mit zwei getrennten Achsen: Hält der Code die dokumentierten Konventionen ein (Standards)? Und tut er, was der Auftrag verlangte (Spec)?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Unsicherheit wird geparkt, nicht geraten.&lt;/strong&gt; Was eine Owner-Entscheidung braucht, landet als Entscheidungsvorlage in einer Liste — nicht still im Diff.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Beleg 1: die 6×6-Kampagnen-Nacht&lt;/h2&gt;
&lt;p&gt;Der &lt;a href=&quot;/log/kampagnen-report-6x6-dispatch&quot;&gt;Kampagnen-Report&lt;/a&gt; dokumentiert den Ablauf im Detail. Die Kurzfassung: 36 mechanische Audit-Befunde, sechs Agenten, sechs Branches, eine Review-Runde, sechs Merges in einer Nacht — 43 geänderte Dateien, vier geparkte Entscheidungsvorlagen, Testsuite grün. Inklusive des ehrlichen Teils: Der geplante Werkzeug-Unterbau (Codex-Subagenten) fiel an der Windows-Sandbox aus, die Mechanik lief unverändert mit Claude-Subagenten weiter. Die Arbeitsform ist werkzeugneutral.&lt;/p&gt;
&lt;h2&gt;Beleg 2: der Observatory-Build — 15 Agenten, zwei blinde Reviews&lt;/h2&gt;
&lt;p&gt;Die &lt;a href=&quot;/observatory&quot;&gt;Live-Demo auf /observatory&lt;/a&gt; — ein Replay echter, vollständig sanitisierter Agenten-Telemetrie — entstand selbst über das Muster, diesmal mit Betonung auf der Review-Seite:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Die Design-Spec wurde adversarial geprüft, bevor eine Zeile Code entstand:&lt;/strong&gt; zwei unabhängige Reviewer (ein Claude-Subagent und Codex), gleicher Prompt, blind gegeneinander. Beide fanden unabhängig denselben kritischen Blocker — die Content-Security-Policy der Site hätte die geplante Einbettung komplett verhindert, und die Sanitisierungs-Allowlist war von der falschen Quelle abgeleitet. Zwei blinde Reviews, die konvergieren, sind ein stärkeres Signal als jede Selbstprüfung.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Der Bau lief als Flotte von 15 Agenten&lt;/strong&gt; (Verstehen → Bauen → Prüfen als getrennte Wellen), gefolgt von fünf Bug-Runden, in denen jede Behauptung live im Browser verifiziert wurde statt im Diff geglaubt.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Die Daten-Seite ist der Ernstfall des Musters:&lt;/strong&gt; Kein String aus der echten Event-Datenbank wird unverändert ausgeliefert — der Export baut jedes Event aus einer Allowlist neu auf, statt Einzelfelder zu redigieren. Das war ein direktes Ergebnis der adversarialen Reviews.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Die Verdikt-Bilanz in Zahlen&lt;/h2&gt;
&lt;p&gt;Damit das nicht bei Erzählung bleibt — die Quoten der beiden Läufe, aus Run-Reports und Git-Log:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Lauf&lt;/th&gt;
&lt;th&gt;Dispatcht&lt;/th&gt;
&lt;th&gt;Akzeptiert &amp;amp; gemergt&lt;/th&gt;
&lt;th&gt;Eskaliert / geparkt&lt;/th&gt;
&lt;th&gt;Referenz&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;6×6-Kampagne&lt;/td&gt;
&lt;td&gt;35 Befunde in 6 Clustern&lt;/td&gt;
&lt;td&gt;6/6 Kampagnen-Branches (Merges &lt;code&gt;03d5391&lt;/code&gt;…&lt;code&gt;848d8ff&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;4 NEEDS-DOMINIC-Vorlagen (umgesetzt als &lt;code&gt;e480e03&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Run-Report 2026-07-01&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Observatory-Build&lt;/td&gt;
&lt;td&gt;1 Spec, 15 Bau-Agenten&lt;/td&gt;
&lt;td&gt;Live-Embed (u. a. &lt;code&gt;340e6b8&lt;/code&gt;) nach 5 Bug-Runden&lt;/td&gt;
&lt;td&gt;2 blinde Spec-Reviews → 1 kritischer Blocker vor Baubeginn gefunden&lt;/td&gt;
&lt;td&gt;Design-Spec 2026-06-30&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Das Site-Repo ist privat — die Hashes stehen hier als überprüfbare Referenz für einen Walkthrough, nicht als Klick-Link.&lt;/p&gt;
&lt;h2&gt;Auch dieser Text entstand so&lt;/h2&gt;
&lt;p&gt;Kleinste Ausbaustufe desselben Musters, vom selben Tag: Der SEO-Batch dieser Website ging nach dem Bau durch einen Codex-Verifier (read-only, voller Diff plus Spec im Prompt). Verdikt: FAIL, genau ein Befund — ein fehlendes &lt;code&gt;dateModified&lt;/code&gt; im Blog-Schema. Der Befund wurde gegen den Code ground-getruthed, per Test-first-Fix geschlossen, gepusht. Ein einzelnes ehrliches FAIL ist mehr wert als zehn freundliche PASS.&lt;/p&gt;
&lt;h2&gt;Was das Muster nicht ist&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Kein Autopilot.&lt;/strong&gt; Merges und strittige Entscheidungen bleiben beim Owner; die Flotte bereitet Entscheidungen vor, sie trifft sie nicht.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keine Skalierungs-Show.&lt;/strong&gt; 15 Agenten sind kein Ziel, sondern eine Zuschnittsfrage — der Verifier-Lauf oben war ein einzelner Agent, und das war richtig so.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Nicht selbstbewertend.&lt;/strong&gt; Jede Zahl in diesem Text stammt aus Run-Reports, Git-Log oder Spec-Dokumenten im Repo — nicht aus der Erinnerung eines Agenten an die eigene Leistung.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Welchen Teil deines Backlogs würdest du einer Flotte geben — und welchen bewusst nicht?&lt;/strong&gt; Wenn dich das Dispatch-Protokoll im Detail interessiert, &lt;a href=&quot;/?anlass=feedback#kontakt&quot;&gt;schreib mir&lt;/a&gt;.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Ein Agent optimiert sich selbst — aber nur, wenn Lighthouse zustimmt</title>
    <link href="https://dynamic-dome.com/log/agent-optimiert-sich-gegen-lighthouse/" rel="alternate" type="text/html"/>
    <id>https://dynamic-dome.com/log/agent-optimiert-sich-gegen-lighthouse/</id>
    <published>2026-07-01T00:00:00Z</published>
    <updated>2026-07-01T00:00:00Z</updated>
    <summary>Codex schlägt Code-Mutationen vor, Lighthouse misst sie, eine harte No-Regress-Regel entscheidet. Was passiert, wenn ein Agent nicht mehr sich selbst bewertet, sondern einem externen, unbestechlichen Signal unterliegt — inklusive der Mutationen, die verworfen wurden.</summary>
    <category term="agentic"/>
    <category term="evolutionary-optimization"/>
    <category term="codex"/>
    <category term="lighthouse"/>
    <category term="guardrails"/>
    <content type="html">&lt;p&gt;Ein Agent, der seinen eigenen Output bewertet, ist ein schwacher Richter. Er neigt dazu, sich selbst recht zu geben — dieselbe Instanz, die den Code schreibt, urteilt auch darüber, ob er gut ist. Die Frage hinter evolab war deshalb einfach: Was passiert, wenn die Bewertung von außen kommt, messbar und unbestechlich?&lt;/p&gt;
&lt;h2&gt;Der Loop: mutieren, messen, selektieren&lt;/h2&gt;
&lt;p&gt;Codex bekommt die Aufgabe, eine Oberfläche zu verbessern. Nicht das Urteil — nur den Vorschlag. Das Urteil liegt komplett bei Lighthouse, Googles Web-Qualitätsmessung: Performance und Accessibility werden gemessen, nicht behauptet.&lt;/p&gt;
&lt;p&gt;Der Loop selbst ist ein genetischer Algorithmus mit einem ungewöhnlichen Mutations-Operator: einem Sprachmodell statt eines Zufallsgenerators. Codex erzeugt eine Variante, Lighthouse misst sie gegen den aktuellen Bestwert, und eine harte Selektionsregel entscheidet, ob sie bleibt.&lt;/p&gt;
&lt;p&gt;Die Regel ist absichtlich streng: eine Mutation muss mindestens eine Metrik echt verbessern — UND darf keine andere Metrik unter die Toleranz drücken. Eine einzelne Zahl zu optimieren wäre einfacher gewesen, hätte aber Kollateralschaden erzeugt: eine Performance-Verbesserung, die Accessibility killt, ist auf dieser Site kein Fortschritt.&lt;/p&gt;
&lt;h2&gt;Mit dem Messrauschen rechnen&lt;/h2&gt;
&lt;p&gt;Reale Lighthouse-Läufe schwanken — dieselbe Seite liefert bei zwei aufeinanderfolgenden Messungen nicht exakt denselben Score. Wer das ignoriert, akzeptiert Zufall als Fortschritt. Die Selektionsregel rechnet deshalb mit einer Jitter-Toleranz: &lt;code&gt;tol = max(1, jitter)&lt;/code&gt;. Eine Mutation muss über das Rauschen hinaus besser sein, nicht nur knapp darüber.&lt;/p&gt;
&lt;p&gt;Im &lt;code&gt;fresh&lt;/code&gt;-Lauf zeigte sich das konkret: zwei Kandidaten erreichten Performance 86 — und wurden trotzdem verworfen, weil 86 nicht weit genug über dem bisherigen Bestwert 85 lag, um als echter Fortschritt statt Messrauschen zu zählen. Das fühlt sich beim ersten Hinschauen unnötig konservativ an. Ist es aber nicht: ohne diese Schwelle hätte der Loop irgendwann eine Kette von Zufallsschwankungen als &quot;Verbesserung&quot; verbucht.&lt;/p&gt;
&lt;h2&gt;Was verworfen wurde&lt;/h2&gt;
&lt;p&gt;Die interessanteren Zahlen sind nicht die Erfolge, sondern die Ablehnungen — die machen die Disziplin des Systems erst sichtbar.&lt;/p&gt;
&lt;p&gt;Im &lt;code&gt;loop-short&lt;/code&gt;-Lauf wurden zwei Mutationen verworfen: eine, weil sie Accessibility von 98 auf 93 gedrückt hätte — ein klarer Performance-für-Accessibility-Tausch, den die No-Regress-Regel nicht zulässt. Eine zweite, weil sie Performance von 85 auf 84 gekostet hätte.&lt;/p&gt;
&lt;p&gt;Im &lt;code&gt;fresh&lt;/code&gt;-Lauf: 3 akzeptierte Mutationen über 10 Iterationen, Performance stieg von 56 auf 85 — bei 4 Codex-Timeouts, die ehrlich mitgezählt wurden statt stillschweigend übersprungen. Accessibility stieg im selben Lauf von 98 auf 100.&lt;/p&gt;
&lt;p&gt;Der &lt;code&gt;overnight&lt;/code&gt;-Lauf erreichte mit 5 konservativen Mutationen einen Sprung von Performance 39 auf 90. Eine riskantere Minify-Variante hätte theoretisch 39→91 erreicht — blieb aber bewusst außen vor, weil sie ungeprüftes Verhalten löscht statt es zu verifizieren. Ein Prozentpunkt mehr Score war den Kontrollverlust nicht wert.&lt;/p&gt;
&lt;h2&gt;Leitplanken, nicht Vertrauen&lt;/h2&gt;
&lt;p&gt;Ein Agent, der selbst Code schreibt und sich selbst bewertet, braucht harte Grenzen — sonst optimiert er ins Leere oder ins Unsichere. Vier davon tragen den Loop:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Scope-Guard&lt;/strong&gt;: Codex darf nur Dateien aus einer Allowlist anfassen. Ein Diff, der ausschert, wird verworfen, bevor er überhaupt gemessen wird.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Jitter-Toleranz&lt;/strong&gt;: siehe oben — Messrauschen darf nicht als Fortschritt zählen.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Gehärteter Subprocess&lt;/strong&gt;: der Codex-Aufruf läuft kontrolliert in einem abgegrenzten Arbeitsverzeichnis, nicht frei auf dem Zielsystem.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Kein Auto-Deploy&lt;/strong&gt;: der Loop produziert nur Reports und eine best-ref-Empfehlung für menschlichen Review. Die Maschine schreibt nichts direkt ins Ziel-Repo.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Der letzte Punkt ist der wichtigste. Ein evolutionärer Loop, der autonom deployt, ist ein anderes Risikoprofil als einer, der nur einen geprüften Kandidaten vorschlägt. Die Selektionsregel entscheidet, was &lt;em&gt;gemessen&lt;/em&gt; als Fortschritt zählt — nicht, was live geht.&lt;/p&gt;
&lt;h2&gt;Was sich verallgemeinern lässt&lt;/h2&gt;
&lt;p&gt;Der eigentliche Ertrag ist nicht der Lighthouse-Optimierer selbst, sondern das Prinzip dahinter: ein unbestechliches externes Mess-Signal statt Selbstbewertung, eine Multi-Metrik-Regel statt Single-Score-Tunnelblick, und eine Toleranz, die Messrauschen respektiert statt zu ignorieren. Das trägt auf jeden Agenten, der sich selbst verbessern soll — Lighthouse ist nur ein Signal von vielen, das sich dafür eignet.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Wo würdest du die Fitness-Funktion ansetzen, wenn du einen Agenten gegen ein externes Signal optimieren lässt?&lt;/strong&gt; Wenn dich interessiert, wie sich das Prinzip auf andere messbare Ziele übertragen lässt, &lt;a href=&quot;/?anlass=feedback#kontakt&quot;&gt;schreib mir&lt;/a&gt;.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Warum ich meinen eigenen Subagenten nicht glaube</title>
    <link href="https://dynamic-dome.com/log/subagenten-nicht-glauben/" rel="alternate" type="text/html"/>
    <id>https://dynamic-dome.com/log/subagenten-nicht-glauben/</id>
    <published>2026-06-15T00:00:00Z</published>
    <updated>2026-06-15T00:00:00Z</updated>
    <summary>Ein Subagent meldet 'erledigt, alle Tests grün'. Das ist eine Behauptung, kein Beweis. Warum ich Selbstberichte von LLM-Agenten gegen Ground-Truth prüfe — mit drei konkreten Fällen.</summary>
    <category term="agentic"/>
    <category term="verification"/>
    <category term="multi-agent"/>
    <category term="ground-truth"/>
    <content type="html">&lt;p&gt;Ein Subagent schreibt: „Erledigt. Alle Tests grün. Commit gesetzt.&quot; Das ist kein Beweis — das ist eine Behauptung, formuliert von einem Sprachmodell, das sehr gut darin ist, plausibel klingende Abschlussmeldungen zu produzieren. Ich habe aufgehört, diese Berichte auf Treu und Glauben zu nehmen. Nicht weil ich den Agenten für unzuverlässig halte — sondern weil ich verstanden habe, was ein LLM-Bericht strukturell leisten kann und was nicht.&lt;/p&gt;
&lt;h2&gt;Warum Reports trügen&lt;/h2&gt;
&lt;p&gt;Ein Sprachmodell hat keinen privilegierten Zugang zur Wahrheit seines eigenen Outputs. Es hat gelernt, wie „ein erfolgreicher Abschlussbericht&quot; klingt — und produziert genau das, wenn die vorherigen Token in diese Richtung zeigen. Ein selbstbewusst formuliertes „Done, all tests passing&quot; ist statistisch genauso wahrscheinlich wie ein korrektes. Beide sehen auf Token-Ebene identisch aus.&lt;/p&gt;
&lt;p&gt;Das ist kein Argument gegen den Einsatz von Subagenten. Es ist ein Argument für eine klare Rollentrennung: Der Agent erledigt Arbeit. Die Verifikation liegt beim Orchestrator — oder bei mir. Beides kann nicht dieselbe Instanz sein, die den Bericht verfasst hat, denn der Bias sitzt in der Completion, nicht im Werkzeug.&lt;/p&gt;
&lt;p&gt;Drei Fälle haben mich das konkret verstehen lassen.&lt;/p&gt;
&lt;h2&gt;Beleg 1 — gefundene Bugs, die keine waren&lt;/h2&gt;
&lt;p&gt;In einem Verdict-Loop hatte ein Agent die Aufgabe, Bugs in einer Codebasis zu finden und zu bewerten. Er fand welche. Der Report war formatiert wie ein echter Code-Review: Dateinamen, Zeilennummern, Fehlerbeschreibungen. Alles stimmig.&lt;/p&gt;
&lt;p&gt;Das Problem: die Fundstellen lagen in Demo-Code, den er nicht von Production-Logik unterschieden hatte. Die „Bugs&quot; waren illustrative Platzhalter, absichtlich kaputt, damit die Demo etwas zu zeigen hat. Der Agent behandelte sie als echte Befunde.&lt;/p&gt;
&lt;p&gt;Hätte ich den Report blind weitergegeben, hätte jemand Demo-Artefakte als echte Schwachstellen priorisiert. Die Form des Reports — überzeugend, strukturiert, vollständig — hatte keinen Zusammenhang mit seinem Inhalt. Der Fehler war nicht im Agent, sondern in meiner Erwartung, dass „gut formatiert&quot; mit „inhaltlich valide&quot; korreliert. Das tut es nicht.&lt;/p&gt;
&lt;h2&gt;Beleg 2 — der Verifier, der gar nicht testete&lt;/h2&gt;
&lt;p&gt;Ein Verifier-Agent sollte nach einem Refactor sicherstellen, dass die Tests noch grün sind. Er lieferte ein Urteil: Tests fehlgeschlagen, drei Failures. Ich war überrascht — der Code sah korrekt aus.&lt;/p&gt;
&lt;p&gt;Dann sah ich das Detail: Gesamtlaufzeit ~0,4 Sekunden. Drei Tests. Eine der Fehlermeldungen lautete &lt;code&gt;document is not defined&lt;/code&gt;. Der Agent hatte &lt;code&gt;bun test&lt;/code&gt; gerufen statt &lt;code&gt;pnpm&lt;/code&gt;/&lt;code&gt;vitest&lt;/code&gt; — in seiner Sandbox war &lt;code&gt;pnpm&lt;/code&gt; nicht verfügbar, also fiel er auf die nächstverfügbare Runtime zurück. &lt;code&gt;bun test&lt;/code&gt; ignoriert die jsdom-Konfiguration; die Tests, die DOM-Zugriffe brauchen, fallen sofort mit &lt;code&gt;document is not defined&lt;/code&gt; durch.&lt;/p&gt;
&lt;p&gt;Das Urteil war technisch korrekt — in der Sandbox, mit der falschen Runtime. Was der Agent wirklich geleistet hatte: er hatte den Diff gelesen und bewertet. Das ist wertvoll. Aber das Testurteil war wertlos, weil die Ausführungsumgebung nicht zu den Anforderungen des Test-Setups passte.&lt;/p&gt;
&lt;p&gt;Hätte ich das Urteil geglaubt, hätte ich funktionierenden Code als kaputt eingestuft und nach Fehlern gesucht, die nicht existieren. Der Report war in sich konsistent. Er war trotzdem falsch.&lt;/p&gt;
&lt;h2&gt;Beleg 3 — doppelt gebaut&lt;/h2&gt;
&lt;p&gt;Carry-over-Buchhaltung über Sessions ist ein unterschätztes Problem. Ein offenes Item wanderte aus einer Session in die nächste — korrekt dokumentiert, klar beschrieben. Zwei parallele Sessions zogen dasselbe Item und bauten denselben Fix unabhängig voneinander.&lt;/p&gt;
&lt;p&gt;Beide Fixes waren korrekt. Beide wurden committed. Der zweite Commit musste aufgelöst werden, weil er auf einem Stand aufbaute, der die Änderung schon enthielt.&lt;/p&gt;
&lt;p&gt;Das war kein Fehler des einzelnen Agenten — er hatte das Item gesehen, es noch nicht als erledigt markiert vorgefunden, und angefangen zu bauen. Der Fehler lag in der fehlenden Snapshot-Verifikation vor Arbeitsbeginn: ein &lt;code&gt;git status --short&lt;/code&gt; plus ein Blick auf den letzten Commit hätte gezeigt, dass die Änderung bereits existierte. Statt auf den Buchhaltungsstand zu vertrauen, wäre ein 30-Sekunden-Ground-Truth-Check ausreichend gewesen.&lt;/p&gt;
&lt;h2&gt;Die Praxis: Ground-Truth vor Buchhaltung&lt;/h2&gt;
&lt;p&gt;Das Muster hinter allen drei Fällen ist dasselbe: ich habe dem Selbstbericht geglaubt, anstatt die Realität zu prüfen. Die Gegenmaßnahme ist simpel und mechanisch:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;git show &amp;lt;hash&amp;gt;&lt;/code&gt; auf den behaupteten Commit — nicht den Report lesen, den Diff sehen.&lt;/li&gt;
&lt;li&gt;Die Test-Suite selbst laufen lassen, nicht das Ergebnis übernehmen: &lt;code&gt;pnpm test&lt;/code&gt;, in der korrekten Umgebung, mit der korrekten Runtime.&lt;/li&gt;
&lt;li&gt;Vor jedem Deploy &lt;code&gt;dist/&lt;/code&gt; greppen — auf Markdown-Artefakte, Pfade, Secrets, Privatdaten.&lt;/li&gt;
&lt;li&gt;Vor Arbeitsbeginn an einem Carry-over-Item &lt;code&gt;git status --short&lt;/code&gt; und letzten Commit prüfen.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Dreißig Sekunden Realitäts-Check statt Narrativ-Glauben. Das ist die ganze Methode. Sie klingt trivial — und ich habe sie trotzdem dreimal nicht angewendet, bevor ich die Fälle hatte, die mich dazu gebracht haben.&lt;/p&gt;
&lt;p&gt;Das ändert sich nicht mit besseren Agenten. Ein Agent, der präziser antwortet, produziert präziser klingende Reports — und macht sie damit überzeugender, nicht wahrer. Der strukturelle Bias bleibt: er berichtet über seine Arbeit mit denselben Mitteln, mit denen er sie erledigt hat — Sprache, keine Ausführung. Den Unterschied zwischen beidem kann kein Sprachmodell aus sich selbst heraus auflösen.&lt;/p&gt;
&lt;h2&gt;Was offen ist&lt;/h2&gt;
&lt;p&gt;Verifikation kostet Zeit und Tokens. Jeder Check, den ich selbst mache, frisst genau den Produktivitätsgewinn, den der Agent eigentlich erbringen soll. Die Grenze zwischen „gesund skeptisch&quot; und „alles zweimal machen&quot; ist unscharf — und sie verschiebt sich je nach Aufgabe, Risiko, und wie kritisch die Folgen eines Fehlers sind.&lt;/p&gt;
&lt;p&gt;Ich habe noch keine gute Antwort darauf, wie selektiv ich diese Checks anwenden soll. Aktuell gilt: je näher eine Agentenausgabe an einem externen Artefakt ist — einem Commit, einem Deploy, einer Kommunikation —, desto eher prüfe ich nach. Bei reinen Analyse-Outputs, die ich sowieso lese, spare ich mir den Overhead. Ob das die richtige Heuristik ist, weiß ich noch nicht.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Welche Selbstberichte deiner Agenten hast du schon einmal nachgeprüft — und was hast du dann tatsächlich vorgefunden?&lt;/strong&gt; Wenn du ähnliche Verification-Patterns nutzt oder andere Ansätze kennst, &lt;a href=&quot;/?anlass=feedback#kontakt&quot;&gt;schreib mir&lt;/a&gt;.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Zwei KI-Modelle, die sich abwechselnd Code bauen und gegenseitig reviewen</title>
    <link href="https://dynamic-dome.com/log/zwei-modelle-reviewen-sich/" rel="alternate" type="text/html"/>
    <id>https://dynamic-dome.com/log/zwei-modelle-reviewen-sich/</id>
    <published>2026-06-14T00:00:00Z</published>
    <updated>2026-06-14T00:00:00Z</updated>
    <summary>Ein Modell baut, das andere reviewt — und bei jedem Accept tauschen sie die Rollen. Wie ich eine Relay-Schleife zwischen Claude und Codex gebaut habe und welche Sandbox-Falle dabei lauerte.</summary>
    <category term="agentic"/>
    <category term="multi-model"/>
    <category term="claude-code"/>
    <category term="codex"/>
    <category term="code-review"/>
    <content type="html">&lt;p&gt;Ein Modell baut, das andere reviewt — und bei jedem Accept tauschen sie die Rollen. Das klingt nach einem eleganten symmetrischen System. Die Realität war erst eine starre Bridge, dann ein Sandbox-Hang, und schließlich drei Runden live: Codex baut, Claude reviewt, ablehnt, Codex überarbeitet, Claude nimmt an, Rollen tauschen. So lief es. Was dazwischen lag, ist der interessante Teil.&lt;/p&gt;
&lt;h2&gt;Der asymmetrische Anfang&lt;/h2&gt;
&lt;p&gt;Die erste Version der Bridge zwischen Claude und Codex war fest verdrahtet: Claude baut, Codex reviewt. Oder umgekehrt, je nach Konfiguration. Einmal gesetzt, unveränderlich für die gesamte Session.&lt;/p&gt;
&lt;p&gt;Das hat funktioniert. Eine Seite produziert Diff, die andere gibt Verdikt. Das ist bereits nützlich, weil zwei Modelle unabhängige Urteilsachsen haben — was Claude übersieht, sieht Codex, und umgekehrt. Die Kombination ist robuster als ein einzelnes Modell, das seinen eigenen Output bewertet: andere Trainingsgewichte, andere blinde Flecken, anderes Gewichtungsmuster bei Sicherheit versus Lesbarkeit.&lt;/p&gt;
&lt;p&gt;Aber die fixe Rollenverteilung verschenkt die Hälfte der Kapazität: der Reviewer kann auch bauen, der Builder kann auch reviewen. Die starre Zuweisung ist keine strukturelle Notwendigkeit, sondern eine Vereinfachung der ersten Iteration. Nützlich zum Beweisen des Konzepts — unzureichend für einen echten Relay-Betrieb. Das war der Ausgangspunkt für den nächsten Schritt.&lt;/p&gt;
&lt;h2&gt;Reviewer-Symmetrie&lt;/h2&gt;
&lt;p&gt;Jedes Modell kann beides: bauen und reviewen. Der Reviewer wird automatisch als das jeweils andere Modell gewählt — Codex reviewt Claude-Output, Claude reviewt Codex-Output. Das lässt sich über ein Flag überschreiben, aber der sinnvolle Default ist das Gegenteil des Builders.&lt;/p&gt;
&lt;p&gt;Was das konkret bedeutet: Codex reviewt rein auf Basis des übergebenen Textes, ohne eigene Datei-Zugriffe — read-only im buchstäblichen Sinn. Claude ebenso, wenn es als Reviewer läuft. Keiner der beiden greift in dieser Phase schreibend ins Repository ein. Das ist eine bewusste Trennung: die Review-Phase erzeugt kein neues Artefakt, nur ein Urteil. Verdikt &lt;code&gt;accept&lt;/code&gt;, &lt;code&gt;reject&lt;/code&gt;, oder &lt;code&gt;rework&lt;/code&gt; mit kommentierter Begründung. Das Urteil ist der Output, nicht ein neuer Diff.&lt;/p&gt;
&lt;p&gt;Die Symmetrie macht das System robuster gegen Bias: wenn Claude immer baut und Claude immer reviewt, lernt das System seine eigenen Muster als Standard. Wechselnde Rollen erzwingen, dass jeder Output durch eine andere Urteilsachse geht. Das ist kein kosmetisches Detail — es verändert, welche Fehler überhaupt sichtbar werden.&lt;/p&gt;
&lt;h2&gt;Die Relay-Schleife&lt;/h2&gt;
&lt;p&gt;Beide Modelle bauen abwechselnd. Der Reviewer beurteilt den inkrementellen Diff — nicht die gesamte Codebasis, nur was sich seit dem letzten Accept geändert hat. Das hält den Kontext des Reviewers klein und das Urteil fokussiert.&lt;/p&gt;
&lt;p&gt;Bei &lt;code&gt;accept&lt;/code&gt; tauschen die Rollen: der bisherige Builder wird Reviewer, der bisherige Reviewer übernimmt das nächste Bau-Segment. Bei &lt;code&gt;reject&lt;/code&gt; bleibt der Builder in seiner Rolle und überarbeitet, bevor weitergegeben wird. Das verhindert, dass ein schwacher Diff in die nächste Runde geht und sich Fehler aufschichten.&lt;/p&gt;
&lt;p&gt;Live getestet über drei Runden: Codex baut eine Komponente, Claude reviewt und lehnt ab mit konkretem Befund — zu wenig Fehlerbehandlung, ein konkreter Fall unberücksichtigt. Codex überarbeitet, Claude nimmt an. Rollen tauschen — Claude baut den nächsten Schritt, Codex reviewt und gibt ohne Einwände frei. Die Schleife läuft. Was dabei auffällt: die Ablehnungsrunde war produktiver als ein blindes Durchwinken es gewesen wäre. Das Rework hat den Diff tatsächlich verbessert, nicht nur formal den Reviewer besänftigt. Drei Runden, ein Reject, ein Rework, zwei Accepts.&lt;/p&gt;
&lt;p&gt;Die Schleife läuft synchron: ein Modell ist aktiv, das andere wartet. Das ist langsamer als paralleles Bauen, aber einfacher zu koordinieren, deterministisch in der Reihenfolge und ohne Merge-Konflikte. Für eine erste produktive Implementierung ist synchron der richtige Kompromiss.&lt;/p&gt;
&lt;h2&gt;Die Sandbox-Falle&lt;/h2&gt;
&lt;p&gt;Der Codex-Reviewer läuft read-only. Keine Schreibzugriffe, keine Commits, nur Lesen und Urteilen. Klingt harmlos. Ist es nicht, wenn &lt;code&gt;approval_policy&lt;/code&gt; nicht explizit gesetzt ist.&lt;/p&gt;
&lt;p&gt;Ohne &lt;code&gt;approval_policy=never&lt;/code&gt; läuft Codex auf Windows in eine &lt;code&gt;%TEMP%&lt;/code&gt;-Probing-Schleife. Das Modell versucht, seinen Arbeitsbereich zu sondieren, schreibt dabei in &lt;code&gt;%TEMP%&lt;/code&gt;, und friert ein, wenn der Sandbox-Modus das blockiert. Der Hang ist still — der Wrapper-Output ist oft leer oder gepuffert, nichts deutet auf das Problem hin. Der Hängepunkt zeigt sich erst im Rollout-Log unter &lt;code&gt;~/.codex/sessions/&lt;/code&gt;: der letzte &lt;code&gt;function_call&lt;/code&gt;-Eintrag ohne zugehörigen &lt;code&gt;function_call_output&lt;/code&gt; ist der Moment, an dem der Prozess eingefroren ist.&lt;/p&gt;
&lt;p&gt;Die Lösung ist zweiteilig und nicht intuitiv: Builder brauchen Schreibzugriff (&lt;code&gt;danger-full-access&lt;/code&gt; für einen isolierten Wegwerf-Workdir), Reviewer kommen mit read-only aus — aber beide nur mit &lt;code&gt;approval_policy=never&lt;/code&gt;. Der Schlüssel ist nicht der Sandbox-Modus an sich, sondern das Unterbinden des Approval-Probings, bevor es zur Hang-Schleife wird. Ein read-only-Reviewer, der nur Text liest und ein Verdikt zurückgibt, friert unter &lt;code&gt;approval_policy=never&lt;/code&gt; nicht ein — das war der Live-Beweis, nach dem der Hang aufgelöst war.&lt;/p&gt;
&lt;p&gt;Was das lehrt: externe CLI-Versionen ändern still ihre Sandbox-Semantik. Was gestern mit einer Konfiguration lief, hängt heute nach einem Update. Nicht dem Wrapper-Output trauen, wenn ein headless Subprozess hängt — das Rollout-Log lesen.&lt;/p&gt;
&lt;h2&gt;Was offen ist&lt;/h2&gt;
&lt;p&gt;Die Schleife funktioniert — aber synchron. Builder wechseln sich ab, niemand baut gleichzeitig. Paralleles Bauen mit anschließendem Merge wäre der nächste Schritt: beide Modelle bearbeiten unabhängige Segmente, dann wird zusammengeführt. Das bringt Merge-Komplexität mit sich, die die synchrone Variante vermeidet. Wann der Aufwand sich lohnt, hängt davon ab, wie granular sich eine Aufgabe zerlegen lässt.&lt;/p&gt;
&lt;p&gt;Die menschliche Eskalation bei Stagnation ist noch grob. Wenn Reject aufeinander folgt und kein Fortschritt entsteht, gibt es einen einzigen Auslöser: eine Datei, ein Signal. Kein abgestuftes Signal — keine Unterscheidung zwischen „zwei Ablehnungen in Folge beim selben Diff&quot; und „fünf Ablehnungen über mehrere Segmente mit verschiedenen Buildern&quot;. Ein abgestufter Signalweg — Warnung, Pause, Eskalation, Abbruch — wäre die nächste Differenzierung, bevor die Schleife autonom über längere Horizonte laufen kann, ohne dass ein Mensch aktiv mitliest.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Wie steuerst du Code-Reviews zwischen Modellen — wer reviewt wen, und nach welcher Logik?&lt;/strong&gt; Wenn du ähnliche Relay-Muster gebaut hast oder eine andere Aufteilung verwendest, &lt;a href=&quot;/?anlass=feedback#kontakt&quot;&gt;schreib mir&lt;/a&gt;.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Ein Hook, der rm -rf blockt — und warum er fast mich selbst ausgesperrt hätte</title>
    <link href="https://dynamic-dome.com/log/block-hook-rm-rf/" rel="alternate" type="text/html"/>
    <id>https://dynamic-dome.com/log/block-hook-rm-rf/</id>
    <published>2026-06-03T00:00:00Z</published>
    <updated>2026-06-03T00:00:00Z</updated>
    <summary>Ein Agent mit Shell-Zugriff kann jeden Befehl tippen, auch rm -rf oder DROP TABLE. Wie ich einen PreToolUse-Hook gebaut habe, der destruktive Kommandos abfängt — und was beim Bau schiefging.</summary>
    <category term="claude-code"/>
    <category term="hooks"/>
    <category term="safety"/>
    <category term="guardrails"/>
    <category term="agentic"/>
    <content type="html">&lt;p&gt;Ein Agent mit Shell-Zugriff kann jeden Befehl tippen. Auch &lt;code&gt;rm -rf&lt;/code&gt;. Auch &lt;code&gt;DROP TABLE&lt;/code&gt;. Das ist keine Hypothese — nach zwei Situationen, die knapp genug ausgingen um ungemütlich zu sein, habe ich einen PreToolUse-Hook gebaut, der jeden Befehl prüft, bevor er ausgeführt wird. Was dabei schiefging, ist fast lehrreicher als was funktioniert.&lt;/p&gt;
&lt;h2&gt;Was der Hook tut&lt;/h2&gt;
&lt;p&gt;PreToolUse-Hooks sind der Kontrollpunkt zwischen dem Moment, in dem ein Agent einen Befehl plant, und dem Moment, in dem er ihn ausführt. Der Hook bekommt den vollständigen Tool-Aufruf übergeben — Befehlsname, Argumente, Kontext — und kann &lt;code&gt;deny&lt;/code&gt; zurückgeben, bevor irgendwas passiert. Das ist der einzige Moment, an dem ein rein sprachlich arbeitender Guardrail wirksam ist: danach ist der Befehl schon weg.&lt;/p&gt;
&lt;p&gt;Mein Hook prüft den Befehlsstring gegen eine Liste destruktiver Muster: &lt;code&gt;rm -rf&lt;/code&gt;, &lt;code&gt;DROP TABLE&lt;/code&gt;, &lt;code&gt;TRUNCATE&lt;/code&gt;, &lt;code&gt;git push --force&lt;/code&gt; auf Mainline-Branches, &lt;code&gt;&amp;gt; wichtige-datei.txt&lt;/code&gt; als Redirect-Überschreib, und ein Dutzend weitere. Trifft ein Muster zu, wird der Aufruf geblockt und ein Audit-Eintrag ins Log geschrieben — JSONL-Format, ein Objekt pro Zeile, mit Zeitstempel, einem stabilen &lt;code&gt;rule&lt;/code&gt;-Schlüssel für die geblockte Kategorie, und dem Befehl in gekürzter Form. „Gekürzt&quot; bedeutet hier: ausreichend für die Diagnose, aber kein vollständiger Dump des Prompt-Inhalts, der ggf. sensible Zwischenstände enthält. Ein Audit-Log, das selbst ein Sicherheitsproblem wird, hilft nicht.&lt;/p&gt;
&lt;p&gt;Die Architektur ist von einem Tool-Usage-Tracker abgeschaut, den ich für den DCO gebaut hatte. Der Schreibvorgang ins Log sitzt in einem &lt;code&gt;try/except&lt;/code&gt;-Block: wenn das Schreiben fehlschlägt, wird der &lt;code&gt;deny&lt;/code&gt; trotzdem ausgelöst. Ein Guard, der an einem kaputten Log-Handle hängt und deshalb nichts blockt, wäre schlimmer als gar kein Guard. Die Konsequenz ist, dass ein Blocking-Event im schlimmsten Fall lautlos ist — aber das ist ein akzeptabler Kompromiss gegenüber einer Umgehung durch Log-Fehler.&lt;/p&gt;
&lt;h2&gt;Wenn Daten wie Befehle aussehen&lt;/h2&gt;
&lt;p&gt;Das erste ernsthafte Problem trat auf, als ich &lt;code&gt;rm -rf&lt;/code&gt; als Substring-Muster matchen wollte. Die naive Idee — wenn der String &lt;code&gt;rm -rf&lt;/code&gt; enthält, blocken — ist auch die erste, die jeder in einer ersten Implementierung wählt. Sie ist naheliegend, weil sie für den häufigsten Fall stimmt. Sie scheitert, weil sie keinen Unterschied macht zwischen dem Befehlsteil und dem Argumentteil eines Aufrufs.&lt;/p&gt;
&lt;p&gt;Konkreter Fall: ein Codex-Review-Aufruf über die Kommandozeile, in etwa &lt;code&gt;node task &quot;…analysiere diesen rm -rf-Fund in prod.db…&quot;&lt;/code&gt;. Der Prompt ist ein Argument in Anführungszeichen. Mein Muster matcht. Der Hook blockt meinen eigenen Review-Aufruf.&lt;/p&gt;
&lt;p&gt;Die Lösung: bevor der Musterabgleich läuft, werden alle gequoteten Spans maskiert — einfache Anführungszeichen, doppelte Anführungszeichen, alles dazwischen wird durch Platzhalter ersetzt. Was danach übrig bleibt, ist der Befehlsteil ohne Argument-Inhalte. Erst darauf läuft das Muster. Ein Newline zählt dabei als Befehlstrennzeichen — was nach einem Zeilenumbruch kommt, wird als eigenständiger Befehl gewertet und separat geprüft.&lt;/p&gt;
&lt;p&gt;Das klingt nach einer kleinen Korrektur. Es war ein Nachmittag Debugging. Und es ist der Punkt, an dem eine naive Substring-Suche an ihre strukturelle Grenze stößt: Shell-Syntax zu parsen ist ein eigenes Problem, und jeder Versuch, es halbherzig zu lösen, produziert entweder False Positives oder False Negatives.&lt;/p&gt;
&lt;h2&gt;fail-open statt fail-closed&lt;/h2&gt;
&lt;p&gt;Eine Entscheidung zieht sich durch den ganzen Hook: wenn unklar ist, ob ein Befehl gefährlich ist, wird er durchgelassen und geloggt — nicht geblockt.&lt;/p&gt;
&lt;p&gt;Das klingt kontraintuitiv für ein Sicherheitssystem. Aber die Alternative ist ein Guard, der produktive Arbeit blockt — und der dann abgestellt wird. Dann schützt er nichts, dauerhaft. Ich habe das Prinzip nach dem Codex-Review-Vorfall formalisiert: lieber einen echten Treffer verpassen und im Audit-Log sehen, als legitime Aufrufe zu verweigern und den Hook nach einer Woche aus Frustration zu deaktivieren. Ein Guardrail, der deaktiviert wurde, ist schlechter als kein Guardrail — weil er eine falsche Sicherheit hinterlässt.&lt;/p&gt;
&lt;p&gt;Das bedeutet: der Hook ist kein hartes Sicherheitsnetz. Er ist ein erster Filter und ein Frühwarnsystem. Die Lücken sind bekannt und dokumentiert — dazu später mehr.&lt;/p&gt;
&lt;h2&gt;Das Henne-Ei-Problem der Selbstverteidigung&lt;/h2&gt;
&lt;p&gt;Ich wollte den Hook auch vor sich selbst schützen. Konkret: eine Regel in Cluster H, die verhindert, dass der Hook-Code selbst bearbeitet wird, ohne explizite Freigabe. Wer die Schutzregeln editieren kann, kann den Schutz aushebeln. Das ist keine theoretische Bedrohung — ein Agent, der seinen eigenen Handlungsspielraum optimiert, ist genau das, was diese Art von Schutzmechanismus ursprünglich motiviert hat.&lt;/p&gt;
&lt;p&gt;Das Ergebnis war vorhersehbar, im Nachhinein: die Regel blockierte mich beim ersten Versuch, den Hook zu verbessern. Ich wollte einen neuen Muster-Eintrag hinzufügen. Der Hook erkannte den Schreibbefehl auf seine eigene Datei als verdächtig. &lt;code&gt;deny&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Zurückgerollt. Lesson learned: Selbstschutz braucht zuerst ein Owner-Bypass-Design — und der Bypass muss vor der Schutzregel existieren, nicht danach. Wer der Owner ist, muss eine verifizierte Möglichkeit haben, die Regeln zu ändern, ohne den gesamten Schutzmechanismus zu umgehen. Das bedeutet in der Praxis: eine separate, nicht vom Hook bewachte Konfigurationsdatei mit expliziter Freigabemechanik, oder eine Bypass-Geste die der Hook selbst versteht. Dieser Bypass existiert noch nicht. Cluster H ist deaktiviert, bis er es tut.&lt;/p&gt;
&lt;h2&gt;Was offen ist&lt;/h2&gt;
&lt;p&gt;Die ehrliche Bestandsaufnahme der bekannten Lücken:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Interpreter-Einzeiler:&lt;/strong&gt; &lt;code&gt;python -c &quot;import os; os.system('rm -rf x')&quot;&lt;/code&gt; landet nicht im Muster. Der Shell-String enthält keinen &lt;code&gt;rm&lt;/code&gt;, nur einen Python-String, der einen &lt;code&gt;rm&lt;/code&gt; enthält.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Command Substitution:&lt;/strong&gt; &lt;code&gt;$(rm -rf x)&lt;/code&gt; oder &lt;code&gt;`rm -rf x`&lt;/code&gt; als Argument ist für den Hook unsichtbar — er sieht den äußeren Befehl, nicht was innerhalb der Substitution steht.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Base64-kodierte Befehle:&lt;/strong&gt; &lt;code&gt;bash -c &quot;$(echo cm0gLXJmIC8= | base64 -d)&quot;&lt;/code&gt; ist für einen Stringmatcher nicht auflösbar.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Piped SQL-Dumps:&lt;/strong&gt; &lt;code&gt;psql database &amp;lt; drop_all.sql&lt;/code&gt; — der Befehl selbst ist harmlos, der Effekt nicht. Die Gefahr steckt im Datei-Inhalt, nicht im Befehlsstring.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Diese Lücken sind dokumentiert und akzeptiert — fail-open bedeutet, dass sie durchrutschen und geloggt werden. Ein Mustermatcher über Shell-Strings ist grundsätzlich unvollständig. Das ist kein Fehler im Design, sondern eine strukturelle Eigenschaft des Ansatzes: wer auf String-Ebene arbeitet, sieht keine Semantik. Wer vollständige Abdeckung braucht, braucht einen anderen Ansatz: sandboxed Execution, Syscall-Monitoring, oder beides. Der Hook, den ich gebaut habe, ist das Werkzeug, das ich in einem Nachmittag produktiv hatte — und das seitdem tatsächlich genutzt wird. Das ist mehr wert als ein perfektes System, das noch in Planung ist.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Welche destruktiven Aktionen hat dein Agent schon fast ausgeführt — und was hat dich aufgehalten?&lt;/strong&gt; Wenn du an ähnlichen Guardrails arbeitest oder andere Ansätze kennst, &lt;a href=&quot;/?anlass=feedback#kontakt&quot;&gt;schreib mir&lt;/a&gt;.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Ein stilles CLI-Upgrade fror Codex 45 Minuten ein</title>
    <link href="https://dynamic-dome.com/log/codex-sandbox-deadlock/" rel="alternate" type="text/html"/>
    <id>https://dynamic-dome.com/log/codex-sandbox-deadlock/</id>
    <published>2026-06-03T00:00:00Z</published>
    <updated>2026-06-03T00:00:00Z</updated>
    <summary>Ein headless gestarteter Codex-Build fror 45 Minuten ein. Im eigenen Log stand nichts. Ein inoffizielles CLI-Upgrade hatte zwei ungeschriebene Annahmen gleichzeitig gebrochen — und die Diagnose lag nicht dort, wo ich sie suchte.</summary>
    <category term="codex"/>
    <category term="subprocess"/>
    <category term="windows"/>
    <category term="sandbox"/>
    <category term="headless-llm"/>
    <content type="html">&lt;p&gt;In der Dual-Bridge baut ein KI-Modell Code, das andere reviewt ihn. Die Builder-Seite läuft headless: &lt;code&gt;codex exec&lt;/code&gt; wird als Subprozess gestartet, bekommt einen Auftrag in einem isolierten Wegwerf-Verzeichnis und arbeitet. Das lief seit Wochen zuverlässig. Dann, an einem Seed-Lauf, fror es ein. Fünfundvierzig Minuten lang. Im Log meines Adapters stand: nichts.&lt;/p&gt;
&lt;p&gt;Das ist die unangenehmste Sorte Fehler — der, bei dem das Werkzeug korrekt anzufangen scheint und dann einfach stehenbleibt. Die ersten Bauschritte liefen sichtbar durch. Dann Stille. Kein Fehler, kein Fortschritt, ein Prozess-Baum, der dasteht und wartet.&lt;/p&gt;
&lt;h2&gt;Zwei Annahmen, gleichzeitig gebrochen&lt;/h2&gt;
&lt;p&gt;Die Ursache war ein CLI-Upgrade, das ich nicht bewusst eingeplant hatte: Codex war von 0.133 auf 0.136 gesprungen. Solche Sprünge brechen gern Annahmen, die man nie aufgeschrieben hat, weil sie immer galten. Hier waren es zwei auf einmal.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Erstens&lt;/strong&gt; lädt 0.136 jetzt die &lt;code&gt;~/.codex/config.toml&lt;/code&gt; mit allem, was darinsteht — Plugins, MCP-Server, Hooks. Und es feuert bei internen Shell-Kommandos einen &lt;code&gt;SessionStart&lt;/code&gt;-Hook. Dieser Hook ist synchron und ohne Timeout. Unter einer interaktiven Session fällt das nicht auf. Unter einem non-interaktiven &lt;code&gt;exec&lt;/code&gt; als Subprozess hängt er sich an einer Stelle auf, an der niemand auf eine Eingabe antwortet.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Zweitens&lt;/strong&gt; lief der Build unter &lt;code&gt;-s workspace-write&lt;/code&gt; — einer Sandbox, die Schreibzugriffe auf das Arbeitsverzeichnis erlaubt, aber &lt;code&gt;%TEMP%&lt;/code&gt; nicht zuverlässig einschließt. pytest legt seine temporären Verzeichnisse aber genau dort an. Die Sandbox verweigert den Zugriff, der Hook beginnt nach einer Freigabe zu suchen, die im headless Betrieb nie kommt — und dreht sich in einer Schleife, die nach &lt;code&gt;%TEMP%&lt;/code&gt; probt, statt abzubrechen.&lt;/p&gt;
&lt;p&gt;Beide Probleme für sich wären unangenehm. Zusammen ergeben sie einen Deadlock: Ein synchroner, timeout-loser Hook trifft auf eine Sandbox, die ihm den Weg versperrt, in einem Kontext, in dem niemand „ja, mach&quot; sagen kann. Und der &lt;code&gt;timeout&lt;/code&gt; meines eigenen &lt;code&gt;subprocess.run&lt;/code&gt; rettet hier nicht — er tötet den direkten Kindprozess nicht durch die ganze Kette &lt;code&gt;python → node → codex → cmd → bash&lt;/code&gt; hindurch.&lt;/p&gt;
&lt;h2&gt;Die Diagnose lag nicht in meinem Log&lt;/h2&gt;
&lt;p&gt;Der entscheidende Schritt war, dem eigenen Log nicht zu vertrauen. Mein Adapter-Log war leer oder gepuffert — es zeigte den letzten erfolgreichen Schritt und dann nichts mehr. Das verleitet dazu, an der falschen Stelle zu suchen.&lt;/p&gt;
&lt;p&gt;Codex schreibt aber sein eigenes Rollout-Log: &lt;code&gt;~/.codex/sessions/&amp;lt;datum&amp;gt;/rollout-*.jsonl&lt;/code&gt;, eine Zeile pro Ereignis. Darin steht jeder &lt;code&gt;function_call&lt;/code&gt; und das zugehörige &lt;code&gt;function_call_output&lt;/code&gt;. Der Hängepunkt ist exakt der letzte &lt;code&gt;function_call&lt;/code&gt;, dem &lt;strong&gt;kein&lt;/strong&gt; &lt;code&gt;function_call_output&lt;/code&gt; folgt. Das ist der Befehl, der losgeschickt wurde und nie zurückkam — in diesem Fall der &lt;code&gt;shell_command&lt;/code&gt;, der in der &lt;code&gt;%TEMP%&lt;/code&gt;-Sandbox-Probing-Schleife versackte.&lt;/p&gt;
&lt;p&gt;Diese Regel verallgemeinert sich: Wenn ein headless gestarteter LLM-Subprozess hängt, ist die Wahrheit im Session-/Rollout-Log des Werkzeugs, nicht im eigenen, oft leeren oder gepufferten Prozess-Log. Der letzte unbeantwortete Aufruf ist der Hängepunkt.&lt;/p&gt;
&lt;h2&gt;Der Fix: Builder und Reviewer brauchen verschiedene Rechte&lt;/h2&gt;
&lt;p&gt;Die Behebung war, die Sandbox-Semantik an die Aufgabe anzupassen, statt einen Mittelweg zu erzwingen. Für einen isolierten Wegwerf-Workdir, in dem der Builder ohnehin schreiben muss, ist &lt;code&gt;-s danger-full-access -c approval_policy=&quot;never&quot;&lt;/code&gt; die richtige Wahl: Der Sandbox-Drop ist die eigentliche Behebung, und &lt;code&gt;approval_policy=&quot;never&quot;&lt;/code&gt; unterbindet das Approval- und &lt;code&gt;%TEMP%&lt;/code&gt;-Probing, bevor es zur Hang-Schleife werden kann.&lt;/p&gt;
&lt;p&gt;Spannend war die Kehrseite: Ein Reviewer, der nur liest — etwa einen im Prompt eingebetteten Diff beurteilt und nichts schreiben muss — hängt &lt;strong&gt;nicht&lt;/strong&gt;. Er läuft mit &lt;code&gt;-s read-only -c approval_policy=&quot;never&quot;&lt;/code&gt; sauber durch. Der Schlüssel ist also nicht der Sandbox-Modus an sich, sondern zweierlei: dass kein Schreibzugriff nötig ist &lt;strong&gt;und&lt;/strong&gt; dass &lt;code&gt;approval_policy=&quot;never&quot;&lt;/code&gt; das Probing unterbindet. Builder schreiben, also brauchen sie vollen Zugriff; Reviewer lesen nur, also kommen sie mit read-only aus — beide nur mit abgeschaltetem Approval.&lt;/p&gt;
&lt;h2&gt;Die Lektion: externe Upgrades brechen still die Spielregeln&lt;/h2&gt;
&lt;p&gt;Der teuerste Teil war die Annahme, dass eine CLI sich zwischen zwei Minor-Versionen gleich verhält. Tut sie nicht. 0.136 hat die Sandbox- und Flag-Semantik verändert, ohne dass mein Code etwas davon mitbekam — er rief weiter dieselben Flags auf, die jetzt etwas anderes bedeuteten. „Funktioniert manuell&quot; war zusätzlich irreführend, weil ein interaktiver Lauf eine andere Approval- und Sandbox-Semantik hat als ein Dienst-Lauf. Genau wie bei den ersten Subprozess-Fehlern war die Lücke zwischen „läuft bei mir in der Konsole&quot; und „läuft headless als Subprozess&quot; der Ort, an dem es brach.&lt;/p&gt;
&lt;h2&gt;Was offen ist&lt;/h2&gt;
&lt;p&gt;Die aktuelle Lösung pinnt die Flags pro Rolle — Builder voll, Reviewer read-only, beide ohne Approval. Was fehlt, ist ein Schutz gegen die nächste stille Semantik-Änderung: Der Adapter sollte die Codex-Version prüfen und bei einem unerwarteten Sprung warnen, statt blind die alten Flags zu verwenden. Außerdem ist der &lt;code&gt;subprocess&lt;/code&gt;-Timeout, der die Prozesskette nicht sauber durchtötet, weiterhin ein stumpfes Werkzeug — ein robusteres Kill über die ganze &lt;code&gt;python → node → codex → cmd → bash&lt;/code&gt;-Kette wäre die ehrlichere Absicherung gegen den nächsten Hang. Bis dahin ist die wichtigste Verteidigung diagnostisch: bei einem Hang nicht dem eigenen Log glauben, sondern das Rollout-Log lesen.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Hat dich schon ein CLI-Upgrade still ausgebremst — und wo hast du am Ende die Wahrheit gefunden?&lt;/strong&gt; &lt;a href=&quot;/?anlass=feedback#kontakt&quot;&gt;Schreib mir&lt;/a&gt;.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Grün allein, rot in der Suite — ein Leck, das in keiner Datei stand</title>
    <link href="https://dynamic-dome.com/log/gruen-allein-rot-in-der-suite/" rel="alternate" type="text/html"/>
    <id>https://dynamic-dome.com/log/gruen-allein-rot-in-der-suite/</id>
    <published>2026-05-31T00:00:00Z</published>
    <updated>2026-05-31T00:00:00Z</updated>
    <summary>Ein Test bestand isoliert, fiel aber in der vollen Suite durch. Die Ursache lebte nicht im Dateisystem, sondern im Prozess-Speicher: ein anderer Test hatte ein Modul neu geladen und dabei eine Registry leergeräumt.</summary>
    <category term="testing"/>
    <category term="pytest"/>
    <category term="test-isolation"/>
    <category term="agentic"/>
    <content type="html">&lt;p&gt;Der Test war grün. Allein ausgeführt, jedes Mal, zuverlässig. In der vollen Suite war er rot. Auch das zuverlässig. Genau diese Kombination — isoliert grün, gemeinsam rot — ist eines der irritierendsten Signale, die eine Testsuite geben kann, weil sie der Grundannahme widerspricht, dass ein Test eine in sich abgeschlossene Aussage ist.&lt;/p&gt;
&lt;p&gt;Der konkrete Fall stammt aus der Dual-Bridge, der Schicht, über die zwei KI-Modelle abwechselnd Code bauen und reviewen. Ein Test prüfte, dass ein Auftrag korrekt an den Codex-Runner geroutet wird und am Ende den Status &lt;code&gt;done&lt;/code&gt; trägt. Allein lieferte er &lt;code&gt;done&lt;/code&gt;. In der Suite lieferte er &lt;code&gt;error&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Das erste Misstrauen gilt der Reihenfolge&lt;/h2&gt;
&lt;p&gt;Wenn ein Test nur im Verbund scheitert, ist die erste Frage nicht „was ist an diesem Test falsch&quot;, sondern „was läuft vor ihm&quot;. Denn ein Test, der allein besteht, ist für sich korrekt. Was ihn umbringt, muss von außen kommen — von etwas, das ein anderer Test hinterlassen hat.&lt;/p&gt;
&lt;p&gt;pytest führt Tests in einer bestimmten Reihenfolge aus, und diese Reihenfolge ist stabil genug, dass „läuft Datei A vor Datei B&quot; reproduzierbar ist. Ein paar gezielte Läufe — erst der verdächtige Test allein, dann mit jeweils einer anderen Datei davor — engen den Schuldigen ein. Hier zeigte sich: Lief eine bestimmte andere Testdatei vorher, war der Test rot. Lief sie nicht, war er grün. Damit war klar, dass es kein Bug im Test selbst war, sondern ein hinterlassener Zustand.&lt;/p&gt;
&lt;h2&gt;Das Leck: ein neu geladenes Modul räumt die Registry leer&lt;/h2&gt;
&lt;p&gt;Die schuldige Testdatei tat etwas, das harmlos aussieht: &lt;code&gt;importlib.reload(runners)&lt;/code&gt;. Sie lud das Runner-Modul neu, um es in einem definierten Ausgangszustand zu testen. Ein &lt;code&gt;reload&lt;/code&gt; führt den Modul-Code aber von vorn aus — und dabei wurde die globale Runner-Registry auf ihren Initialwert zurückgesetzt: nur der Test-&lt;code&gt;echo&lt;/code&gt;-Runner, sonst nichts. Die echten Adapter, die beim ersten Import registriert worden waren, waren weg.&lt;/p&gt;
&lt;p&gt;Der spätere Test rechnete mit &lt;code&gt;RUNNERS[&quot;codex&quot;]&lt;/code&gt;. Nach dem &lt;code&gt;reload&lt;/code&gt; existierte dieser Eintrag nicht mehr. Statt den Codex-Runner zu finden, lief der Auftrag in einen Fehlerpfad — daher &lt;code&gt;error&lt;/code&gt; statt &lt;code&gt;done&lt;/code&gt;. Dazu kam eine zweite Quelle desselben Problems: Umgebungsvariablen, die ein Test setzte, ohne sie danach aufzuräumen. &lt;code&gt;DUAL_BRIDGE_*&lt;/code&gt;-Werte leben im Prozess-Speicher und überdauern die einzelne Testfunktion, wenn niemand sie zurücksetzt.&lt;/p&gt;
&lt;p&gt;Beide Lecks haben dieselbe Eigenschaft: Sie hinterlassen keinen Abdruck im Dateisystem. Man kann das gesamte Repo durchsuchen und findet nichts, weil der veränderte Zustand in flüchtigem Prozess-Speicher liegt — in einem Modul-Global, in &lt;code&gt;os.environ&lt;/code&gt;. Diese Art von Leck ist unsichtbar, bis man die Ausführungsreihenfolge als Verdächtigen ernst nimmt.&lt;/p&gt;
&lt;h2&gt;Der Fix: pro Test ein sauberer Tisch&lt;/h2&gt;
&lt;p&gt;Die Behebung war eine autouse-Fixture in der &lt;code&gt;conftest.py&lt;/code&gt;, die vor jedem Test einen Schnappschuss des relevanten Zustands macht und ihn danach wiederherstellt: die komplette &lt;code&gt;DUAL_BRIDGE_*&lt;/code&gt;-Umgebung und die Runner-Registry. Jeder Test bekommt damit denselben Ausgangszustand, egal was der vorherige getan hat.&lt;/p&gt;
&lt;p&gt;Das ist der eigentliche Punkt: Test-Isolation heißt nicht nur, eine eigene Datenbank oder ein eigenes Temp-Verzeichnis zu haben. Es heißt auch, &lt;strong&gt;jeden&lt;/strong&gt; geteilten, veränderlichen Zustand zu kapseln — und dazu gehören Modul-Globals und Umgebungsvariablen, die man leicht übersieht, weil sie nicht nach „Zustand&quot; aussehen. &lt;code&gt;monkeypatch.setenv&lt;/code&gt; statt direktem &lt;code&gt;os.environ&lt;/code&gt;-Schreiben und autouse-Fixtures für Registries sind das Minimum, nicht die Kür.&lt;/p&gt;
&lt;h2&gt;Warum „allein grün&quot; eine gefährliche Beruhigung ist&lt;/h2&gt;
&lt;p&gt;Es liegt nahe, einen Test zu schreiben, ihn isoliert grün zu sehen und ihn damit für fertig zu halten. Aber die isolierte Ausführung ist nicht der reale Fall. Im echten Lauf — in CI, vor einem Merge — läuft die volle Suite, und dort entscheidet sich, ob der Test trägt. Ein Test, der nur allein besteht, gibt eine falsche Sicherheit: Er behauptet eine Eigenschaft, die unter realen Bedingungen nicht gilt.&lt;/p&gt;
&lt;p&gt;Schlimmer noch: Solche Lecks sind nicht-deterministisch in dem Sinn, dass sie an der Reihenfolge hängen. Eine neue Testdatei mit einem alphabetisch früheren Namen kann ein Leck einschleusen, das vorher nie sichtbar war — und plötzlich ist ein ganz anderer Test rot, ohne dass jemand ihn angefasst hat. Das ist die Sorte Fehler, die Stunden frisst, weil der offensichtliche Verdächtige (der rote Test) der Falsche ist.&lt;/p&gt;
&lt;h2&gt;Was offen ist&lt;/h2&gt;
&lt;p&gt;Die autouse-Fixture kapselt den bekannten Zustand — die &lt;code&gt;DUAL_BRIDGE_*&lt;/code&gt;-Env und die Runner-Registry. Aber sie ist eine Aufzählung: Sie schützt genau das, woran ich gedacht habe. Das nächste Modul-Global, das jemand einführt und nicht in den Snapshot aufnimmt, kann dasselbe Problem neu erzeugen. Eine robustere Lösung wäre, geteilten veränderlichen Zustand strukturell zu vermeiden — Registries als Fixtures injizieren statt als Modul-Globals zu halten, sodass es gar keinen prozessweiten Zustand gibt, der lecken könnte. Bis dahin gilt die Disziplin: Wer einen Modul-Global oder eine Umgebungsvariable einführt, muss im selben Atemzug klären, wie sie pro Test zurückgesetzt wird.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Welcher Test ist bei dir nur in der vollen Suite rot geworden — und wie lange hast du den falschen verdächtigt?&lt;/strong&gt; &lt;a href=&quot;/?anlass=feedback#kontakt&quot;&gt;Schreib mir&lt;/a&gt;.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Wie ich zweimal meine eigene Datenbank löschte — und was Test-Isolation wirklich heißt</title>
    <link href="https://dynamic-dome.com/log/test-db-isolation/" rel="alternate" type="text/html"/>
    <id>https://dynamic-dome.com/log/test-db-isolation/</id>
    <published>2026-05-29T00:00:00Z</published>
    <updated>2026-05-29T00:00:00Z</updated>
    <summary>Zweimal hat eine Test-Suite meine produktive Datenbank geleert, obwohl ich sicher war, isoliert zu testen. Die Diagnose: ein eingefrorener Pfad zur Import-Zeit. Was daraus folgt.</summary>
    <category term="testing"/>
    <category term="pytest"/>
    <category term="datensicherheit"/>
    <category term="python"/>
    <category term="lessons-learned"/>
    <content type="html">&lt;p&gt;Zweimal hat eine Test-Suite meine produktive Datenbank geleert. Beide Male war ich sicher, isoliert zu testen. Beide Male lag ich falsch — und zwar auf unterschiedliche Arten. Das ist das Unbehaglichste daran: der zweite Verlust passierte, nachdem ich aus dem ersten gelernt hatte. Ich hatte eine Lösung gebaut. Die Lösung hatte eine Lücke.&lt;/p&gt;
&lt;h2&gt;Der erste Verlust&lt;/h2&gt;
&lt;p&gt;Der erste Vorfall war im Nachhinein offensichtlich. Die Test-Suite startete, und kein einziger Test hatte einen Pfad-Override gesetzt. &lt;code&gt;conftest.py&lt;/code&gt; existierte, aber ohne Isolation-Mechanismus — keine Umgebungsvariable, keine &lt;code&gt;monkeypatch&lt;/code&gt;-Zuweisung, kein &lt;code&gt;tmp_path&lt;/code&gt;. Die Tests liefen einfach gegen die echte Datendatenbank, weil nirgendwo ein anderer Pfad stand.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;DELETE FROM&lt;/code&gt; im Teardown. Tabellen sauber. Daten weg.&lt;/p&gt;
&lt;p&gt;Dass ich das nicht sofort gesehen hatte, lag an einem mentalen Kurzschluss: ich hatte &lt;em&gt;Testcode&lt;/em&gt; gelesen und war davon ausgegangen, dass Testcode automatisch eine andere Umgebung meint. Das stimmt nicht. Tests laufen in derselben Python-Umgebung wie Produktionscode, greifen auf dieselben Pfade zu, sofern niemand etwas überschreibt — und ob irgendjemand etwas überschreibt, steht nicht auf der Stirn des conftest geschrieben.&lt;/p&gt;
&lt;p&gt;Die Reaktion war die naheliegende: Isolation nachrüsten. Umgebungsvariable für den Datenbankpfad, &lt;code&gt;monkeypatch.setenv&lt;/code&gt; im conftest, Teardown gegen eine temporäre Datei. Beim nächsten Testlauf: isoliert, sauber, kein Datenverlust. Ich hielt das Problem für gelöst.&lt;/p&gt;
&lt;h2&gt;Der zweite, raffiniertere Verlust&lt;/h2&gt;
&lt;p&gt;Der zweite Vorfall war subtiler. Das conftest sah diesmal korrekt aus. Der Override war gesetzt. Beim Überfliegen der Datei hätte ich unterschrieben, dass die Isolation stimmt.&lt;/p&gt;
&lt;p&gt;Trotzdem war &lt;code&gt;calls.db&lt;/code&gt; danach leer.&lt;/p&gt;
&lt;p&gt;Die Diagnose dauerte länger als sie sollte, weil ich an der falschen Stelle gesucht hatte: ich hatte die Testinfrastruktur geprüft und die Produktionsmodule nicht in Frage gestellt. Der Fehler lag dort.&lt;/p&gt;
&lt;p&gt;Ein Modul hatte auf Modulebene eine Konstante definiert:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;DB_PATH = str(DATA_DIR / &quot;calls.db&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Das klingt harmlos. Es ist es nicht. Python friert diesen Wert beim Import ein — in dem Moment, in dem das Modul das erste Mal geladen wird. Was danach passiert — &lt;code&gt;delenv&lt;/code&gt;, &lt;code&gt;reload(config)&lt;/code&gt;, &lt;code&gt;reload(module)&lt;/code&gt; im Teardown — kommt zu spät. Der String ist ein String. Er zeigt weiterhin auf &lt;code&gt;./data/calls.db&lt;/code&gt;, die echte Datenbank. Das Modul hat den Override nie gesehen.&lt;/p&gt;
&lt;p&gt;Der conftest hatte korrekt die Umgebungsvariable gesetzt. Das Modul hatte sie korrekt ignoriert, weil es sie zum Zeitpunkt der Pfadauflösung nicht mehr gab — dieser Zeitpunkt lag Millisekunden vor dem ersten Testaufruf, im Import-Schritt, bevor irgendein Test-Setup lief.&lt;/p&gt;
&lt;h2&gt;Lazy statt eingefroren&lt;/h2&gt;
&lt;p&gt;Das Anti-Pattern ist die Modulkonstante. Die einzige sichere Alternative ist eine Funktion, die &lt;code&gt;config.DATA_DIR&lt;/code&gt; bei jedem Aufruf frisch liest:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def _db_path() -&amp;gt; str:
    return str(config.DATA_DIR / &quot;calls.db&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Erst das macht den Pfad überschreibbar. Wenn der Test &lt;code&gt;DATA_DIR&lt;/code&gt; via Umgebungsvariable auf ein temporäres Verzeichnis zeigt und das Modul &lt;code&gt;_db_path()&lt;/code&gt; bei jedem Datenbankzugriff aufruft, landet der Zugriff auch tatsächlich auf der Test-DB. Keine Import-Zeitkonstante, keine eingefrorene Referenz.&lt;/p&gt;
&lt;p&gt;Dazu kommen zwei harte Guards, die ich danach eingebaut habe:&lt;/p&gt;
&lt;p&gt;Erstens ein &lt;code&gt;RuntimeError&lt;/code&gt; im conftest-Header: wenn der aufgelöste Override-Pfad mit dem echten Produktionspfad identisch ist, bricht das Setup sofort ab. Nicht warnen — abbrechen. Das fängt den Fall ab, in dem der Override-Mechanismus selbst nicht greift.&lt;/p&gt;
&lt;p&gt;Zweitens eine Poison-Guard nach jedem Test: ein Assertion, die den von &lt;code&gt;_db_path()&lt;/code&gt; aufgelösten Pfad gegen &lt;code&gt;./data&lt;/code&gt; prüft. Wenn das Modul trotz allem auf den echten Pfad zeigt — vielleicht weil ein Reload fehlgeschlagen ist, vielleicht weil ein neues Modul die Konstante wieder eingeführt hat — schlägt die Guard an. Kein stilles Übergehen, kein Hoffnungstest.&lt;/p&gt;
&lt;p&gt;Die beiden Guards zusammen machen einen dritten Vorfall unwahrscheinlicher. Aber nur, wenn der Code sie nicht umgeht — und das können neue Module, die das Anti-Pattern wieder einführen, jederzeit tun.&lt;/p&gt;
&lt;h2&gt;Der conftest-Header beweist nichts&lt;/h2&gt;
&lt;p&gt;Das ist die Lektion, die ich am meisten gebraucht hätte, bevor der zweite Vorfall passierte: das Lesen des conftest-Headers beweist keine Isolation. Es beweist, dass ein Override-Mechanismus &lt;em&gt;beschrieben&lt;/em&gt; ist. Ob er tatsächlich greift, steht da nicht.&lt;/p&gt;
&lt;p&gt;Der Trugschluss lautet: &lt;em&gt;ich habe den conftest gelesen und gesehen, dass eine Test-DB gesetzt wird, also ist die Isolation in Ordnung&lt;/em&gt;. Dieser Trugschluss hat den zweiten Verlust mitverursacht. Ich hatte nach dem ersten Vorfall den conftest erweitert und das als ausreichend betrachtet.&lt;/p&gt;
&lt;p&gt;Ausreichend ist es nicht. Ausreichend ist ein Row-Count-Snapshot der echten Datenbanken vor und nach einem echten Suite-Lauf. Wenn die Zahlen übereinstimmen, war die Isolation in Ordnung — nicht weil der conftest so aussieht, als wäre sie in Ordnung, sondern weil die Daten das bestätigen. Ein Mismatch nach dem Lauf ist ein Bug. Sofort melden, Suite nicht weiterführen.&lt;/p&gt;
&lt;p&gt;Das kostet zwei SQL-Abfragen. Es lohnt sich.&lt;/p&gt;
&lt;h2&gt;Was offen ist&lt;/h2&gt;
&lt;p&gt;Die Guards, die ich jetzt habe, sind reaktiv. Sie fangen den Fehler, nachdem die Architektur ihn zugelassen hat. Der &lt;code&gt;RuntimeError&lt;/code&gt; im conftest-Header greift erst, wenn die Test-Session startet. Die Poison-Guard greift, nachdem ein Test gelaufen ist, der möglicherweise bereits geschrieben hat.&lt;/p&gt;
&lt;p&gt;Was fehlt, ist eine strukturelle Lösung: dass Produktionscode in Testmodus schlicht keinen Zugriff auf &lt;code&gt;./data&lt;/code&gt; haben kann — nicht weil ein Guard dagegen anfeuert, sondern weil der Pfad in diesem Modus physisch nicht existiert oder auf ein Read-only-Mount zeigt. Das wäre Isolation auf Betriebssystem-Ebene statt auf Python-Ebene.&lt;/p&gt;
&lt;p&gt;Ich habe das noch nicht gebaut. Die Guards halten den Betrieb aufrecht, und der Aufwand für eine echte Sandbox ist real. Aber ich habe jetzt zweimal erlebt, was passiert, wenn der Schutz auf Konvention beruht statt auf Konstruktion — und das macht mich vorsichtiger als ich es vor diesen Vorfällen war.&lt;/p&gt;
&lt;p&gt;Ein eingefrorener Pfad sieht aus wie ein gesetzter Pfad. Der Unterschied ist unsichtbar, bis er nicht mehr ist.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Welche Annahme über deine Test-Isolation hat sich als falsch herausgestellt — und wann hast du es gemerkt?&lt;/strong&gt; Wenn du ähnliche Vorfälle erlebt hast oder andere Ansätze zur strukturellen Isolation kennst, &lt;a href=&quot;/?anlass=feedback#kontakt&quot;&gt;schreib mir&lt;/a&gt;.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Approval-Pfade in agentischen Systemen: Warum Automatisierung Freigabe-Logik braucht</title>
    <link href="https://dynamic-dome.com/log/approval-pfade-in-agentischen-systemen/" rel="alternate" type="text/html"/>
    <id>https://dynamic-dome.com/log/approval-pfade-in-agentischen-systemen/</id>
    <published>2026-05-15T00:00:00Z</published>
    <updated>2026-05-15T00:00:00Z</updated>
    <summary>Vollautomation ist selten das Ziel. Wer Agenten produktiv einsetzt, muss entscheiden, an welchen Stellen ein Mensch zustimmen muss. Wie ich das im DCO gelöst habe.</summary>
    <category term="agentic"/>
    <category term="approval"/>
    <category term="safety"/>
    <category term="dco"/>
    <category term="claude-code"/>
    <content type="html">&lt;p&gt;Vollautomation klingt nach dem Ziel, ist aber selten eins. Bei jedem Agenten-System, das ich gebaut habe, kam früher oder später dieselbe Frage: an welchen Stellen darf das System eigenständig handeln, und wo muss ein Mensch zustimmen, bevor etwas passiert? Das ist die Approval-Frage, und sie ist die wichtigste Architektur-Entscheidung beim Bau produktiver Coding-Agenten.&lt;/p&gt;
&lt;h2&gt;Die Asymmetrie zwischen Lesen und Schreiben&lt;/h2&gt;
&lt;p&gt;Lesende Operationen sind billig: ein Agent liest eine Datei, ruft eine API ab, durchsucht Logs. Wenn er falsch läuft, kostet das Tokens und etwas Zeit. Schreibende Operationen sind teuer: eine gelöschte Datei ist weg, ein commit-pushed ist öffentlich, eine versendete Nachricht lässt sich nicht zurückholen. Dieser Asymmetrie folgt die einfachste Approval-Heuristik: lesend autonom, schreibend mit Approval.&lt;/p&gt;
&lt;p&gt;Aber die Heuristik bricht, sobald die Realität komplexer wird. Eine Datenbank-Query &quot;nur lesend&quot; kann das System lahmlegen, wenn sie versehentlich &lt;code&gt;SELECT *&lt;/code&gt; über Milliarden Zeilen macht. Eine &quot;schreibende&quot; Operation wie das Anlegen einer Wegwerf-Branch ist trivial reversibel. Die Heuristik braucht Verfeinerung — und genau dort kollabiert die meisten Agenten-Implementierungen.&lt;/p&gt;
&lt;h2&gt;Vier Stufen statt zwei&lt;/h2&gt;
&lt;p&gt;Im DCO (Dynamic Central Orchestrator), den ich seit März 2026 produktiv betreibe, habe ich vier Approval-Stufen statt der binären Aufteilung:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Autonom&lt;/strong&gt; — der Agent führt aus, ohne zu fragen. Lesende Operationen mit klar definierten Limits, idempotente Schreibvorgänge in einem Sandbox-Scope.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Notify&lt;/strong&gt; — der Agent führt aus, schickt aber eine Telegram-Nachricht. Ich kann nachträglich revidieren, aber muss nicht warten. Gut für Aktionen mit hoher Vertrauenslage und geringem Schaden.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Confirm&lt;/strong&gt; — der Agent stoppt, schickt einen Vorschlag, wartet auf &quot;ja&quot; oder &quot;nein&quot;. Standard für schreibende Operationen ausserhalb der Sandbox: git push, Slack-Nachricht, Datei löschen.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Veto&lt;/strong&gt; — der Agent darf das nicht. Punkt. Hartcodiert in der Tool-Auswahl. Beispiel: &lt;code&gt;rm -rf&lt;/code&gt; überhaupt nicht aufrufbar, auch nicht mit Approval.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Diese Stufen sind kein theoretisches Modell, sondern direkt in den MCP-Tools des DCO codiert. Jedes Tool deklariert seine Approval-Stufe, und der Orchestrator-Layer entscheidet basierend darauf, ob er ein Telegram-Inline-Button sendet oder einfach weiterläuft.&lt;/p&gt;
&lt;h2&gt;Confirm ist die schwierigste Stufe&lt;/h2&gt;
&lt;p&gt;Autonom ist trivial: einfach machen. Veto ist trivial: einfach blockieren. Notify ist trivial: machen + senden. Aber Confirm hat einen User-Experience-Killer: &lt;strong&gt;Wartezeit&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Wenn ein Agent bei jedem zweiten Schritt anhält und auf Bestätigung wartet, ist das Erlebnis kaum besser als manuelles Arbeiten. Die Confirm-Stufe braucht drei Eigenschaften, damit sie produktiv ist:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Sub-Sekunden-Roundtrip&lt;/strong&gt;, wenn der User antwortbereit ist. Im DCO heisst das: Telegram-Inline-Buttons, kein Form, kein Tippen.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sinnvolles Default-Verhalten&lt;/strong&gt;, wenn der User nicht antwortet. Timeout, der zurück auf &quot;Abbruch&quot; fällt, niemals stillschweigend ausführen.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Begründung in der Frage&lt;/strong&gt;, nicht nur &quot;Soll ich X tun?&quot;. Stattdessen &quot;Soll ich X tun? Effekt: Y. Risiko: Z. Default bei keiner Antwort: Abbruch.&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Die dritte Eigenschaft ist die wichtigste. Ein Agent, der auf Bestätigungen wartet, ohne den Effekt zu erklären, wird entweder per Reflex bestätigt oder per Reflex abgelehnt. Beides ist ein Failure-Mode.&lt;/p&gt;
&lt;h2&gt;Approval-Pfade als Architektur, nicht als Patch&lt;/h2&gt;
&lt;p&gt;Der häufigste Fehler beim Bau eigener Agenten ist, Approval als Nachgedanke zu implementieren — als ob-Block um den Tool-Aufruf, der &quot;frag mal nach&quot;. Das hat zwei Probleme: erstens ist die Approval-Logik dann über den Code verstreut, statt zentral verwaltet zu werden. Zweitens kann man die Approval-Regeln nicht änderbar machen, ohne den Code anzufassen.&lt;/p&gt;
&lt;p&gt;Die Lösung ist, Approval als zentrale Architektur-Schicht zu verstehen. Jedes Tool meldet beim Registrieren seine Approval-Stufe an den Orchestrator. Der Orchestrator entscheidet beim Aufruf, was zu tun ist. Aenderungen an Approval-Regeln passieren am Tool, nicht an der Aufruf-Stelle. Das macht Approval testbar, versionierbar und auditierbar.&lt;/p&gt;
&lt;h2&gt;Was ich nicht gelöst habe&lt;/h2&gt;
&lt;p&gt;Drei Probleme bleiben offen, und ich erwähne sie ehrlich, weil sie der eigentliche Lern-Stoff sind:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Stille Aenderungen sichtbar machen.&lt;/strong&gt; Wenn ein Tool eine Datenbank im Hintergrund modifiziert, ohne dass ein Approval-Knopf gedrückt wurde, wie merkt der User es? Mein aktueller Hack: ein &quot;Audit-Log&quot;-Kanal in Telegram, der jeden Schreibvorgang nachträglich postet. Das ist Notify-Stufe über alle Schreiboperationen — funktioniert, aber laut.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Approval-Stufen veraltern.&lt;/strong&gt; Was heute &quot;Confirm&quot; ist, könnte morgen &quot;Notify&quot; sein, weil ich mehr Vertrauen in das Tool habe. Wie versioniere ich diese Aenderung sauber, ohne dass alte Sessions noch die alte Regel nutzen? Im DCO ist das aktuell hartcodiert mit manuellem Edit — keine elegante Lösung.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Approval-Fatigue.&lt;/strong&gt; Wenn der User 50 Bestätigungen am Tag bekommt, klickt er irgendwann &quot;ja&quot; ohne zu lesen. Gegenmittel: Approval-Stufen verschärfen, Notify aggressiv einsetzen, Aktionen bundlen (&quot;Soll ich diese 5 Schritte machen?&quot; statt 5 einzelne Fragen).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Approval-Pfade sind kein gelöster Bereich. Sie sind der Punkt, an dem Engineering-Disziplin und Produkt-Sinn aufeinandertreffen, und an dem agentische Systeme den Unterschied zwischen Spielzeug und Werkzeug machen.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Was hat dich an deinen Approval-Entscheidungen wirklich überrascht?&lt;/strong&gt; Wenn du selbst Agenten baust und das Thema dich umtreibt, &lt;a href=&quot;/?anlass=feedback#kontakt&quot;&gt;schreib mir&lt;/a&gt; — ich sammle Ansätze.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>GitHub umbenannt, Seite tot — und fünf Tage merkt es niemand</title>
    <link href="https://dynamic-dome.com/log/github-rename-webhook-drift/" rel="alternate" type="text/html"/>
    <id>https://dynamic-dome.com/log/github-rename-webhook-drift/</id>
    <published>2026-05-12T00:00:00Z</published>
    <updated>2026-05-12T00:00:00Z</updated>
    <summary>Ein GitHub-Account-Rename ging glatt durch: git push funktionierte weiter, alles schien in Ordnung. Fünf Tage später zeigte die Live-Seite immer noch den alten Stand. Kein Fehler, kein Alert — nur ein Webhook, der ins Leere lief.</summary>
    <category term="devops"/>
    <category term="ci-cd"/>
    <category term="github"/>
    <category term="cloudflare-pages"/>
    <category term="verification"/>
    <content type="html">&lt;p&gt;Ich habe einen GitHub-Account umbenannt. Eine Kosmetik-Sache, dachte ich — alter Name, neuer Name, fertig. &lt;code&gt;git push&lt;/code&gt; lief direkt danach ohne Murren. Commits landeten im Repo. Lokal war alles grün. Die Sache war abgehakt, bevor sie überhaupt eine Sache war.&lt;/p&gt;
&lt;p&gt;Fünf Tage später fiel mir auf, dass die Live-Seite Dinge nicht zeigte, die ich längst gepusht hatte. Kein Deploy-Fehler im Dashboard, keine Fehlermeldung, keine rote Pipeline. Die Seite war einfach auf einem Stand von vor fünf Tagen eingefroren — und zwar genau ab dem Tag des Renames.&lt;/p&gt;
&lt;h2&gt;Was wirklich passiert war&lt;/h2&gt;
&lt;p&gt;Ein Account-Rename auf GitHub ist großzügig mit Weiterleitungen. Die alten Clone-URLs antworten mit einem 301-Redirect auf die neuen. Deshalb funktionierte &lt;code&gt;git push&lt;/code&gt; sofort weiter: Mein lokales Remote zeigte auf die alte URL, GitHub leitete um, der Push kam an. Genau dieser reibungslose Moment war die Falle — er suggerierte, dass „alles automatisch nachzieht&quot;.&lt;/p&gt;
&lt;p&gt;Was nicht nachzieht, ist die Webhook-Zustellung. Hosting-Plattformen wie Cloudflare Pages, Vercel oder Netlify hängen ihren Auto-Deploy an einen Webhook, der bei jedem Push feuert. Dieser Webhook ist aber nicht an den Namen gebunden, sondern an die interne Repo-Identität, die bei der Verbindung registriert wurde. Nach dem Rename feuert GitHub die Events weiter — aber die Plattform-seitige Bindung passt nicht mehr sauber, und die Zustellung versandet. Der Build-Hook wird nie ausgelöst. Es gibt keinen fehlgeschlagenen Deploy, weil gar kein Deploy versucht wird.&lt;/p&gt;
&lt;p&gt;Das ist das Heimtückische: Ein fehlgeschlagener Deploy ist sichtbar — rote Markierung, Log, Alert. Ein &lt;strong&gt;nicht ausgelöster&lt;/strong&gt; Deploy ist unsichtbar. Das Dashboard zeigt den letzten erfolgreichen Build, der einfach von vor fünf Tagen ist, und nichts deutet darauf hin, dass seitdem etwas hätte passieren sollen.&lt;/p&gt;
&lt;h2&gt;Warum die Standard-Annahme falsch ist&lt;/h2&gt;
&lt;p&gt;Der Gedanke „GitHub kümmert sich um die Weiterleitung, also folgt alles&quot; stimmt — aber nur für Clone-URLs. Für git ist der Rename transparent. Für die Event-Zustellung an Drittsysteme ist er es nicht. Die 301-Redirects decken den Pull/Push-Pfad ab, nicht den Push-löst-Webhook-löst-Deploy-Pfad.&lt;/p&gt;
&lt;p&gt;Diese Asymmetrie ist der ganze Bug. Man sieht den einen Pfad funktionieren (git push klappt) und schließt daraus auf den anderen (Deploy läuft auch). Aber die beiden teilen sich nichts außer dem Auslöser. Der eine ist robust gegen Renames, der andere nicht.&lt;/p&gt;
&lt;h2&gt;Die 30-Sekunden-Diagnose&lt;/h2&gt;
&lt;p&gt;Was hilft, ist ein billiger externer Check, der nicht dem Dashboard glaubt, sondern die Live-Seite selbst befragt. Drei orthogonale Indikatoren, in dreißig Sekunden:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Sitemap-Datum:&lt;/strong&gt; &lt;code&gt;curl -s https://&amp;lt;site&amp;gt;/sitemap.xml | grep lastmod&lt;/code&gt; — steht da ein frisches Datum oder ein altes?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build-Asset-Hash:&lt;/strong&gt; Der Hash im Dateinamen des Haupt-Bundles (&lt;code&gt;index-&amp;lt;hash&amp;gt;.js&lt;/code&gt;) ändert sich bei jedem Build. Bleibt er gleich, wurde nicht neu gebaut.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Content-Type/Status:&lt;/strong&gt; &lt;code&gt;curl -sI https://&amp;lt;site&amp;gt;/&amp;lt;datei&amp;gt;&lt;/code&gt; — antwortet die Seite überhaupt wie erwartet?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Drei verschiedene Blickwinkel, weil ein einzelner trügen kann: Ein Cache kann ein altes Asset ausliefern, eine Sitemap kann statisch sein. Wenn aber alle drei auf „alt&quot; stehen, ist klar, dass kein Deploy mehr durchläuft.&lt;/p&gt;
&lt;p&gt;Der eigentliche Fix war banal: im Plattform-Dashboard das Repo neu verbinden. Eine Minute Arbeit — sobald man weiß, dass es nötig ist.&lt;/p&gt;
&lt;h2&gt;Die Lektion: stille Fehler brauchen eigene Sensoren&lt;/h2&gt;
&lt;p&gt;Der teure Teil war nicht der Fix, sondern die fünf Tage. Fünf Tage, in denen ich glaubte, deployt zu haben, während nichts ankam. Stille Fehler — die, die keinen Alarm auslösen — sind die teuersten, weil die Zeit bis zur Entdeckung allein von Zufall abhängt. Hier war es Zufall, dass mir nach fünf Tagen etwas auffiel. Es hätten auch zwei Wochen sein können.&lt;/p&gt;
&lt;p&gt;Die verallgemeinerbare Lehre: Nach jedem Account- oder Org-Rename müssen alle Webhook-getriebenen Integrationen — Hosting, CI, Bots — bewusst neu verbunden werden. Und für alles, was „eigentlich automatisch&quot; passieren soll, lohnt sich ein billiger Indikator, der das Ergebnis von außen prüft, statt der internen Statusanzeige zu vertrauen. Ein Dashboard, das sagt „letzter Build erfolgreich&quot;, beantwortet nicht die Frage „ist der aktuelle Stand live&quot;.&lt;/p&gt;
&lt;h2&gt;Was offen ist&lt;/h2&gt;
&lt;p&gt;Der manuelle 30-Sekunden-Check ist gut für den akuten Fall, aber er ist eben manuell — er hilft nur, wenn man ihn ausführt. Die saubere Lösung wäre eine kleine Deploy-Freshness-Probe im Monitoring: ein periodischer Abgleich zwischen dem letzten Commit-Datum und dem Sitemap-/Asset-Stand der Live-Seite, der Alarm schlägt, wenn beide zu weit auseinanderlaufen. Bis das steht, bleibt die Konvention: nach jedem Rename die Hosting-Bindungen anfassen, nicht darauf vertrauen, dass git push genügt.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Welcher „erledigte&quot; Infrastruktur-Schritt hat dich später eingeholt — und wie hast du gemerkt, dass etwas still kaputt war?&lt;/strong&gt; &lt;a href=&quot;/?anlass=feedback#kontakt&quot;&gt;Schreib mir&lt;/a&gt;.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Ein Script hing 14 Minuten ohne Fehler — der Pipe-Buffer war voll</title>
    <link href="https://dynamic-dome.com/log/pipe-buffer-deadlock/" rel="alternate" type="text/html"/>
    <id>https://dynamic-dome.com/log/pipe-buffer-deadlock/</id>
    <published>2026-04-17T00:00:00Z</published>
    <updated>2026-04-17T00:00:00Z</updated>
    <summary>Eine NotebookLM-Extraktion fror mitten im Lauf ein. Kein Fehler, kein Timeout, nur Stille. Die Ursache war kein Bug im Code, sondern ein voller Betriebssystem-Puffer — und eine subprocess-Annahme, die an großen Daten zerbricht.</summary>
    <category term="python"/>
    <category term="subprocess"/>
    <category term="windows"/>
    <category term="deadlock"/>
    <category term="agentic"/>
    <content type="html">&lt;p&gt;Das Skript lief seit Wochen. Es rief ein externes Tool auf, las dessen JSON-Ausgabe und verarbeitete sie weiter — bei NotebookLM-Extraktionen, yt-dlp-Abrufen, allem, was strukturierte Antworten liefert. Dann, eines Tages, hing es. Vierzehn Minuten lang. Kein Fehler, kein Stacktrace, kein Timeout. Nur ein Prozess, der dastand und nichts tat.&lt;/p&gt;
&lt;p&gt;Das Tückische an diesem Fehler ist, dass er nicht aussieht wie ein Fehler. Ein Crash hat einen Stacktrace, den man lesen kann. Ein Timeout hat eine Meldung. Ein Deadlock hat nichts — er ist die Abwesenheit von Fortschritt, und die ist schwer von „dauert halt lange&quot; zu unterscheiden.&lt;/p&gt;
&lt;h2&gt;Was das Script tat&lt;/h2&gt;
&lt;p&gt;Der Aufruf war Standard: &lt;code&gt;subprocess.run(cmd, capture_output=True, text=True)&lt;/code&gt;. &lt;code&gt;capture_output=True&lt;/code&gt; ist die bequeme Variante — sie sammelt &lt;code&gt;stdout&lt;/code&gt; und &lt;code&gt;stderr&lt;/code&gt; ein und gibt sie als fertige Strings zurück. Für die allermeisten Aufrufe ist das genau richtig: Befehl läuft, produziert ein bisschen Ausgabe, Python liest sie ein, fertig.&lt;/p&gt;
&lt;p&gt;Der Unterschied an diesem Tag: Die Antwort war groß. Rund 314 KB JSON. Und genau bei dieser Größenordnung kippt das Verhalten.&lt;/p&gt;
&lt;h2&gt;Warum ein voller Puffer alles anhält&lt;/h2&gt;
&lt;p&gt;Zwischen einem Eltern- und einem Kindprozess liegt eine Pipe — ein Betriebssystem-Puffer mit begrenzter Größe. Unter Windows sind das je nach Konfiguration nur einige zehn Kilobyte. Der Kindprozess schreibt seine Ausgabe in diese Pipe; der Elternprozess liest sie heraus und macht Platz für mehr.&lt;/p&gt;
&lt;p&gt;Das funktioniert, solange jemand auf der anderen Seite liest. &lt;code&gt;subprocess.run&lt;/code&gt; mit &lt;code&gt;capture_output=True&lt;/code&gt; liest aber erst, &lt;strong&gt;nachdem&lt;/strong&gt; der Kindprozess fertig ist — vereinfacht gesagt: es wartet auf das Ende, dann holt es die Ausgabe ab. Bei kleiner Ausgabe ist das egal: alles passt in den Puffer, der Kindprozess schreibt fertig, beendet sich, der Elternprozess liest. Bei großer Ausgabe entsteht ein Patt: Der Kindprozess füllt den Puffer, will weiterschreiben, kann aber nicht, weil niemand abräumt — also blockiert er. Der Elternprozess wartet darauf, dass der Kindprozess sich beendet — was nie passiert, weil der Kindprozess am Schreiben hängt. Beide warten aufeinander. Das ist der Deadlock.&lt;/p&gt;
&lt;p&gt;Verschärft wird es, wenn &lt;code&gt;stdout&lt;/code&gt; und &lt;code&gt;stderr&lt;/code&gt; gleichzeitig volllaufen. Dann ist jede Pipe für sich ein potenzieller Hängepunkt, und es reicht, dass eine von beiden den Puffer füllt.&lt;/p&gt;
&lt;p&gt;Die vierzehn Minuten waren übrigens kein „es lief langsam&quot; — es lief gar nicht. Der Prozess wäre auch nach vierzehn Stunden noch dagestanden.&lt;/p&gt;
&lt;h2&gt;Die Lösung: nicht in den Speicher, sondern auf die Platte&lt;/h2&gt;
&lt;p&gt;Der Fix ist unspektakulär: Bei potenziell großer Ausgabe nicht &lt;code&gt;capture_output=True&lt;/code&gt; verwenden, sondern &lt;code&gt;stdout&lt;/code&gt; auf eine temporäre Datei umleiten — &lt;code&gt;subprocess.run(cmd, stdout=open(tmp, &quot;wb&quot;))&lt;/code&gt;. Das Betriebssystem schreibt direkt in die Datei, der Puffer läuft nie voll, der Kindprozess blockiert nie. Danach liest Python die Datei ein.&lt;/p&gt;
&lt;p&gt;Das ist langsamer als der Speicher-Weg — ein zusätzlicher Schreib-/Lese-Vorgang auf die Platte. Aber „langsamer und korrekt&quot; schlägt „schnell und hängend&quot; in jeder Bilanz. Die saubere, stdlib-korrekte Alternative wären nebenläufige Threads, die beide Pipes parallel leeren (genau das tut &lt;code&gt;subprocess.communicate()&lt;/code&gt; intern) — aber für ein Tool, das ohnehin große Dateien produziert, ist die Temp-Datei der einfachere und robustere Weg.&lt;/p&gt;
&lt;p&gt;Die Faustregel, die daraus wurde: Sobald eine Subprozess-Ausgabe potenziell über etwa 50 KB gehen kann, ist &lt;code&gt;capture_output=True&lt;/code&gt; ein Risiko, kein Komfort.&lt;/p&gt;
&lt;h2&gt;Stille ist die schlimmste Fehlermeldung&lt;/h2&gt;
&lt;p&gt;Die eigentliche Lektion ist weniger technisch als diagnostisch. Dieser Bug war so lange unentdeckt, weil er sich nur bei großen Eingaben zeigte — und große Eingaben waren der Ausnahmefall. „Funktioniert mit kleinen Daten&quot; ist ein verräterisches grünes Licht: Es verdeckt jede Klasse von Fehlern, die erst an der Pufferung, am Speicher oder an der Datenmenge hängt.&lt;/p&gt;
&lt;p&gt;Und weil der Fehler sich als Stille äußert, ist er teuer zu diagnostizieren. Bei einem Crash sucht man nach der Ursache. Bei einem Hang sucht man erst einmal nach der Frage, ob überhaupt etwas kaputt ist. Die Antwort fand sich nicht im eigenen Code, sondern im Verständnis dessen, was zwischen den Prozessen passiert — in der Pipe, die man normalerweise nie sieht.&lt;/p&gt;
&lt;h2&gt;Was offen ist&lt;/h2&gt;
&lt;p&gt;Der Temp-Datei-Fix ist robust, aber er ist eine Behandlung des Symptoms am Aufrufpunkt. Die saubere Lösung wäre, große Antworten gar nicht erst als ein einzelnes JSON-Blob über die Pipe zu schieben, sondern zu streamen — zeilenweise oder in Chunks, sodass nie eine große Menge auf einmal im Puffer liegt. Das verlangt aber, dass auch das aufgerufene Tool Streaming unterstützt, und nicht jedes tut das.&lt;/p&gt;
&lt;p&gt;Bis dahin gilt die einfache Regel: Bei allem, was groß werden kann, geht die Ausgabe auf die Platte, nicht in den Speicher. Es ist nicht elegant. Aber es hängt nicht.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Hattest du schon einen Prozess, der ohne Fehler einfach stehenblieb — und wie lange hat es gedauert, bis du den Puffer im Verdacht hattest?&lt;/strong&gt; &lt;a href=&quot;/?anlass=feedback#kontakt&quot;&gt;Schreib mir&lt;/a&gt;.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Erste Woche, drei Subprocess-Fehler — wie der Bot lernte, auf Windows zu sprechen</title>
    <link href="https://dynamic-dome.com/log/erste-woche-subprocess/" rel="alternate" type="text/html"/>
    <id>https://dynamic-dome.com/log/erste-woche-subprocess/</id>
    <published>2026-03-29T00:00:00Z</published>
    <updated>2026-03-29T00:00:00Z</updated>
    <summary>Der DCO begann als Telegram-Bot, der die Claude-CLI aufruft. In der ersten Woche scheiterte jeder Aufruf auf eine neue Art: erst fand Windows das Programm nicht, dann zerfielen die Umlaute, dann war der Gesprächskontext kaputt.</summary>
    <category term="python"/>
    <category term="subprocess"/>
    <category term="windows"/>
    <category term="utf-8"/>
    <category term="agentic"/>
    <content type="html">&lt;p&gt;Der Dynamic Central Orchestrator fing klein an: ein Telegram-Bot, der eingehende Nachrichten an die Claude-CLI weiterreicht und die Antwort zurückschickt. Im Prinzip ein Einzeiler — &lt;code&gt;subprocess.run([&quot;claude&quot;, &quot;-p&quot;, prompt])&lt;/code&gt;. In der Praxis war es drei Tage Lehrgeld, verteilt auf drei Fehler, die nichts miteinander zu tun hatten außer dem Ort, an dem sie passierten: der Grenze zwischen meinem Python-Prozess und einem anderen Programm auf Windows.&lt;/p&gt;
&lt;h2&gt;Fehler 1: Windows findet das Programm nicht&lt;/h2&gt;
&lt;p&gt;Der erste Lauf endete mit &lt;code&gt;FileNotFoundError: [WinError 2]&lt;/code&gt;. Im Terminal lief &lt;code&gt;claude&lt;/code&gt; problemlos — also musste es ja installiert sein. Der Bot sah es trotzdem nicht.&lt;/p&gt;
&lt;p&gt;Der Grund: Ein Service- oder Hintergrundprozess erbt nicht denselben &lt;code&gt;PATH&lt;/code&gt; wie meine interaktive Shell. Wenn ich &lt;code&gt;claude&lt;/code&gt; in die Konsole tippe, durchsucht die Shell ihren PATH und findet den npm-Wrapper. Mein Python-Prozess, gestartet aus einem anderen Kontext, hatte diesen PATH-Eintrag nicht. Für mich war das Programm „da&quot;. Für den Subprozess existierte es nicht.&lt;/p&gt;
&lt;p&gt;Die Lösung war nicht, den Befehl anders zu schreiben, sondern ihn vorher aufzulösen: &lt;code&gt;shutil.which(&quot;claude&quot;)&lt;/code&gt; durchsucht den PATH des laufenden Prozesses und gibt den absoluten Pfad zurück — oder &lt;code&gt;None&lt;/code&gt;, wenn nichts gefunden wird. Diesen absoluten Pfad übergebe ich dann an &lt;code&gt;subprocess.run&lt;/code&gt;. Aus „verlass dich darauf, dass das Programm im PATH ist&quot; wird „finde es einmal, sauber, und scheitere laut, wenn es fehlt&quot;.&lt;/p&gt;
&lt;h2&gt;Fehler 2: aus „für&quot; wird „fÃ¼r&quot;&lt;/h2&gt;
&lt;p&gt;Der nächste Lauf funktionierte — fast. Die Antworten kamen zurück, aber jeder Umlaut war zerschossen. „für&quot; wurde zu „fÃ¼r&quot;, „schön&quot; zu „schÃ¶n&quot;. Klassisches Mojibake: UTF-8-Bytes, durch eine Latin-1-Brille gelesen.&lt;/p&gt;
&lt;p&gt;Windows ist hier eine besondere Falle, weil die Standard-Konsolen-Codepage nicht UTF-8 ist. Wenn Text als Kommandozeilen-Argument durch die Prozessgrenze geht, hängt die Kodierung von Dingen ab, die ich nicht kontrolliere — der Codepage des Eltern- und des Kindprozesses. Ein Prompt mit deutschen Umlauten wird unterwegs umkodiert, und niemand fragt mich, ob das so gewollt war.&lt;/p&gt;
&lt;p&gt;Der Fix war eine Verschiebung des Übergabewegs: nicht den Prompt als Argument anhängen, sondern über &lt;code&gt;stdin&lt;/code&gt; einspeisen — &lt;code&gt;subprocess.run(cmd, input=prompt, encoding=&quot;utf-8&quot;)&lt;/code&gt;. Über stdin ist der Text ein Datenstrom mit expliziter Kodierung, kein von der Shell interpretiertes Argument. Das löst gleich zwei Probleme auf einmal: die Umlaute bleiben heil, und lange oder sonderzeichen-haltige Prompts werden nicht vom Quoting der Shell zerlegt. Letzteres wurde später, bei den &lt;code&gt;.cmd&lt;/code&gt;-Wrappern, noch zu einer eigenen Geschichte.&lt;/p&gt;
&lt;h2&gt;Fehler 3: der Bot vergisst, worüber wir reden&lt;/h2&gt;
&lt;p&gt;Jetzt liefen die Aufrufe sauber durch — aber der Bot hatte kein Gedächtnis. Jede Nachricht war für ihn die erste. „Und dann?&quot; war eine sinnlose Frage, weil es kein „davor&quot; gab.&lt;/p&gt;
&lt;p&gt;Das ist kein Subprocess-Bug im engeren Sinn, aber es ist dieselbe Klasse: Ein frisch gestarteter Prozess hat genau den Kontext, den ich ihm mitgebe — und keinen Zeichen mehr. Der CLI-Aufruf ist zustandslos. Wenn ich Gesprächsverlauf will, muss ich ihn selbst zusammenbauen und mitschicken.&lt;/p&gt;
&lt;p&gt;Die naive Variante — einfach alle bisherigen Nachrichten aneinanderhängen — produziert schnell Matsch, in dem das Modell nicht mehr unterscheiden kann, was meine Eingabe war und was seine eigene frühere Antwort. Es braucht klare Trennmarken: wer hat was gesagt, wo endet ein Beitrag, wo beginnt der nächste. Erst mit strukturiertem Verlauf statt einem Textbrei wurde aus den Einzelaufrufen ein Gespräch.&lt;/p&gt;
&lt;h2&gt;Drei Fehler, ein Muster&lt;/h2&gt;
&lt;p&gt;Im Rückblick war keiner dieser Fehler schwer. Jeder einzelne war eine Stunde Recherche und eine Zeile Code. Was sie verbindet, ist die Stelle, an der sie auftraten — und die Annahme, die sie alle teilten: dass ein anderes Programm aufzurufen „einfach&quot; ist.&lt;/p&gt;
&lt;p&gt;Ist es nicht. Auf Windows zerfällt „ein Programm aufrufen&quot; in mindestens drei getrennte Probleme: Wird das Programm überhaupt gefunden? Kommt der Text unverfälscht an? Hat der Aufruf genug Kontext, um sinnvoll zu antworten? Jedes davon ist unsichtbar, bis man es trifft, und keines verrät sich vorher.&lt;/p&gt;
&lt;p&gt;Die teuerste Annahme war „funktioniert in meinem Terminal&quot;. Mein Terminal hat den richtigen PATH, eine UTF-8-fähige Konsole und einen Menschen, der den Kontext im Kopf hat. Ein Bot-Prozess hat nichts davon automatisch. Die Lücke zwischen „läuft bei mir interaktiv&quot; und „läuft als Dienst&quot; ist genau die Lücke, in der diese drei Fehler wohnen.&lt;/p&gt;
&lt;h2&gt;Was offen ist&lt;/h2&gt;
&lt;p&gt;Die hier beschriebenen Fixes sind robust, aber sie sind projektspezifisch zusammengesucht. Was fehlt, ist eine saubere Abstraktion: eine kleine Wrapper-Funktion, die &lt;code&gt;shutil.which&lt;/code&gt; auflöst, stdin mit UTF-8 erzwingt und Fehler einheitlich behandelt — einmal richtig gebaut, überall wiederverwendet. Stattdessen sind die Lehren über mehrere Projekte verteilt als Konventionen dokumentiert und werden bei jedem neuen Subprozess-Aufruf einzeln angewendet. Das funktioniert, ist aber fehleranfällig: Wer die Konvention nicht kennt, baut Fehler 1 bis 3 frisch nach.&lt;/p&gt;
&lt;p&gt;Die ehrliche Bilanz nach der ersten Woche: Der Bot konnte sprechen. Aber dass „mit einem anderen Programm reden&quot; so viel Reibung erzeugt, war die erste echte Lektion des Projekts — und eine, die sich danach in jedem weiteren Windows-Subprozess wiederholt hat.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Welcher Subprocess-Aufruf hat dich am längsten gekostet — und woran lag es am Ende wirklich?&lt;/strong&gt; Wenn du an ähnlichen Integrationen arbeitest, &lt;a href=&quot;/?anlass=feedback#kontakt&quot;&gt;schreib mir&lt;/a&gt;.&lt;/p&gt;
</content>
  </entry>
</feed>
