Den größten Teil meiner Arbeit erledigen inzwischen Modelle. Das ist der einfache Teil. Der schwere ist, vorher zu sagen, woran ich erkenne, dass sie fertig ist.
Was an welches Modell geht
Ich führe eine Tabelle mit drei Zahlen je Modell: was es mich kostet, wie schwer ein Problem sein darf, das es unbeaufsichtigt trägt, und Geschmack, womit ich Urteil über Oberflächen, Benennung, API-Zuschnitt und Text meine. Jede Delegationsentscheidung fällt aus diesen dreien. Wie das Routing im Einzelnen funktioniert und warum kein Agent seine eigene Arbeit begutachtet, habe ich separat aufgeschrieben.
- Mechanische Massenarbeit geht an die günstigste Stufe: Extraktion, Klassifikation, Datei-für-Datei-Transformationen, Codemods, i18n-Schlüssel, breite Prüf-Batches.
- Umsetzung nach klarer Spezifikation und Recherche im Repository mit definierter Frage gehen an den Standard-Worker.
- Unklares, Architektur und Sicherheitsrelevantes eskaliert, und alles, woran eine günstigere Stufe schon gescheitert ist.
- Alles, was ein Nutzer sieht, braucht Geschmack ab sieben. Diese eine Regel hält die günstigen Stufen aus Oberflächen, Benennung und Text heraus, und sie blockiert die meisten Läufe.
Die günstigste Stufe ist zuletzt deutlich günstiger geworden, also geht mehr dorthin als noch vor einem Jahr. Klingt nach reinem Gewinn. Genau dort ist mein Prozess gerissen.
Vierzehn grüne Releases
Ich hatte für zwei Starter-Kits die vollständige Kette durchgezogen: Anforderungen, Design, Plan, Umsetzung, jeder Schritt aufgeschrieben, bevor der nächste anfing. Vierzehn Releases gingen raus. typecheck, lint, knip, test, test:boundaries, build, Playwright und axe: durchgehend grün.
Dazwischen hat niemand hineingesehen. Ich habe die nächste Schleife angestoßen, die Gates kamen grün zurück, und ich habe die nächste angestoßen. Das ist der Teil, den man benennen muss: Die Gates haben mich weniger getäuscht, als dass sie mir den Grund genommen haben, hinzusehen.
Dann habe ich mich hingesetzt und die Anwendung selbst bedient.
Ein frischer Klon startete nicht. Jede Seite hinter /app stürzte ab. Das Anmeldeformular schrieb das Passwort im Klartext in die URL, weil es auf onSubmit baute und ein Absenden vor der Hydration nativ als GET durchgeht. Die Onboarding-Liste war Dekoration: jede Schreiboperation starb an einem Fremdschlüssel.
Keines meiner Gates hatte etwas gemerkt, und das war folgerichtig statt überraschend. Sie beantworten alle dieselbe Art von Frage: eine über den Code. Übersetzt er, bündelt er, rendert er. Keines beantwortete eine Frage über das Produkt: Legt eine Anmeldung ein Konto an?
Ein Test, der eine Seite rendert und sie nie bedient, ist ein Screenshot mit Zusatzschritten.
Die zweite Lehre war leiser und unangenehmer. Zwei Anläufe für eine Lint-Regel, die Client-Inseln den Import von Server-Code verbietet, waren beide wirkungslos: einer war schema-ungültig, einer stand außerhalb des Config-Aufrufs. Beide liefen grün und fingen nichts. Ein Gate, das du nie hast fehlschlagen sehen, ist ungetestet. Heute schreibe ich den verbotenen Code als Fixture und sehe der Regel beim Auslösen zu, bevor ich ihr glaube.
Vier Prüfungen, vier Fragen
Herausgekommen ist eine Pipeline, in der jede Stufe eine andere Frage beantwortet, und ich kann sagen, welche:
typecheck,lint,test,build, übersetzt und bündelt der Code?- anti-slop, ein Satz meinungsstarker Oxlint-Regeln: liest sich das wie Modell-Ausgabe, die niemand angesehen hat?
- Greptile, das jeden Pull Request kommentiert: fällt einem zweiten Leser etwas auf? Bei mir hat es echte Sachen gefunden, darunter einen Median über zwei Messwerte, der rechnerisch ein Mittelwert ist.
- Eine Browser-Sonde, die sich registriert, eine Organisation anlegt und sie benutzt, dazu ein Durchlauf über alle Seiten, der null Fehlerseiten und null Konsolenfehler verlangt, ausgeloggt wie eingeloggt: tut das Produkt seine Arbeit?
Nur die letzte fragt nach dem Produkt. Sie ist auch die einzige, die von den vierzehn irgendetwas gefunden hätte.
Deshalb kommt die günstige Stufe mit einer Bedingung statt mit einem Rabatt. Arbeit geht nur dorthin, wenn ich vor dem Start ein maschinell prüfbares Abnahmekriterium benennen kann. Kann ich das nicht, ist es keine günstige Aufgabe, und sie geht an eine Stufe, der ich auch ohne glaube. Günstigere Läufe heißen mehr Ausgabe, die niemand Zeile für Zeile liest, und grüne Gates waren vierzehn Mal hintereinander kein Beleg.
Das gilt übrigens über Agenten hinaus. Ich hatte schon ein Agenten-Review, das einen weitgehend erfundenen Befund einreichte, und die Behauptungen, die stimmten, waren die mit Pfad und Wert, während in den Sätzen über Systemzustände jeder Fehler steckte.
Die Aufgabe schärfen, bevor sie läuft
Die günstigste Stufe ist die, bevor irgendetwas läuft. Ich habe einen Skill, der mich zu einer Aufgabe ausfragt, eine Entscheidung nach der anderen, jede mit einer Empfehlung, bis keine Lücke mehr offen ist: die ungenannte Randbedingung, die Alternative, an die ich nicht gedacht habe, das, von dem ich annahm, es sei ohnehin klar.
Das ist unangenehm und es lohnt sich. Eine Frage kostet mich zwei Minuten. Ein Lauf auf einer falschen Annahme kostet den Lauf, das Review und die Nacharbeit, und die Nacharbeit ist der teure Teil, weil es dann schon Code gibt, mit dem man streiten kann.
Wenn das Ziel messbar ist
Manche Arbeit lässt sich bewerten statt begutachten. Dafür habe ich einen Skill, der die Loss-Function und den Harness entwirft: er befragt die Aufgabe, baut eine verblindete Evaluation, erzeugt den Harness und sucht adversarial nach Wegen, auf denen ein Modell gut abschneidet, ohne die Arbeit zu tun. Diesen letzten Schritt lassen die meisten weg, und er entscheidet, ob die Zahl etwas bedeutet.
Heraus kommt eine Zieldatei, die der Harness dann als langen Optimierungs-Loop fährt. Zwei Dinge, die ich jedem mitgeben würde: Wer das Orakel schreibt, darf nicht das Modell im Loop sein. Und wenn der Loop meldet, der Harness sei fehlerhaft, rechne das unabhängig nach, bevor du den Scorer anfasst. Beide Male, als ein Loop mir sagte, die Loss-Function sei kaputt, hatte er recht, aber ohne Nachrechnen hätte ich es nicht gewusst.
Zwei Klänge
Ein Loop läuft lange, und ihn anzustarren ist Verschwendung eines Menschen. Also zwei Hooks, je eine Zeile Konfiguration:
{
"hooks": {
"Notification": [
{ "matcher": "*", "hooks": [
{ "type": "command", "command": "afplay /System/Library/Sounds/Frog.aiff" }
]}
],
"Stop": [
{ "matcher": "*", "hooks": [
{ "type": "command", "command": "afplay /System/Library/Sounds/Glass.aiff" }
]}
]
}
}
Notification feuert, wenn der Agent mich braucht: eine Eingabe, eine Freigabe. Stop feuert, wenn der Zug fertig ist. Zwei verschiedene Klänge, weil das zwei verschiedene Zustände sind und ich sie ohne Blick auf den Bildschirm auseinanderhalten will.
Darin liegt der ganze Zweck, und er ist kleiner, als er klingt. Das ist kein Benachrichtigungssystem, das ist eine Zustandsanzeige. Ein Klang heißt komm zurück, der andere heißt du bist wieder dran, und dazwischen kann ich etwas anderes tun: was die einzige Art ist, auf die ein langer Loop tatsächlich billiger ist, als die Arbeit selbst zu machen.
Eine Datei, zwei Leser
Die Instruktionen selbst sind das letzte Stück. Verschiedene Werkzeuge lesen verschiedene Dateinamen: die einen suchen AGENTS.md, die anderen CLAUDE.md. Beide von Hand zu pflegen ergibt genau das, was man erwartet: zwei Dateien, die übereinstimmen, bis sie es unbemerkt nicht mehr tun.
Also ist eine die Quelle und die andere erzeugt, mit einer Kopfzeile, die das sagt:
AGENTS.mdschreibe ich.CLAUDE.mdwird daraus erzeugt und trägt eine Nicht-bearbeiten-Zeile.
Es gibt ein Projekt, in dem das nicht funktioniert, und das sollte man wissen, bevor man vereinheitlicht: Next.js schreibt seinen eigenen Block in AGENTS.md und setzt ihn bei jedem next dev wieder hinein. Die andere Datei daraus zu erzeugen würde die Projektregeln durch den Textbaustein des Frameworks ersetzen. Zwei Konventionen wollen denselben Dateinamen, also bleibt der Generator dort aus und der Grund steht daneben.
Wichtiger als die Frage, welche Datei es ist, ist die Frage, was darin steht. Meine beginnt mit vier Regeln, die mit keiner Codebase zu tun haben:
- Fertig heißt fertig. Fünf Dinge gefragt heißt fünf Dinge geliefert. Ist eines wirklich blockiert, werden die anderen vier fertig und der konkrete Blocker steht in einem Satz da.
- Eine Frage ist eine Frage. „Sollten wir X nehmen?" heißt nicht „migriere alles auf X". Erst antworten, handeln auf Zuruf.
- Handeln im Plan, nicht am Plan vorbei. Vor der Umsetzung steht ein Plan. Innerhalb eines freigegebenen Plans wird alles Umkehrbare und Billige gemacht und berichtet, nicht erfragt.
- Antworten in einfacher Sprache: kurze Sätze, eine Anweisung pro Satz. Das gilt nur für Antworten an mich; Commit-Messages und alles mit echtem Publikum behalten ihr eigenes Register.
Die dritte Regel hat zwei Anläufe gebraucht. „Handle, frag nicht" und „plane immer zuerst" lesen sich wie Gegensätze, und ein Agent, dem man beides gibt, nimmt die, zu der der aktuelle Satz gerade passt. Erst die Rangfolge aufzuschreiben, erst der Plan, dann freie Hand darin, hat aus zwei Regeln, die sich stritten, eine gemacht, die trägt.
Next
Die Lücke, die ich nicht geschlossen habe, ist die Browser-Sonde in jedem Projekt statt in den zweien, die sie schon haben. Das Portieren ist nicht die Arbeit: zu entscheiden, was „die Aufgabe des Nutzers" für jedes Produkt heißt, ist die Arbeit, und das ist ein Satz pro Feature, den niemand schreiben will.
Was würdest du als fünfte Prüfung ergänzen? Mich interessiert vor allem, ob jemand eine Prüfung gefunden hat, die die Sorte Fehler fängt, die sich erst zeigt, wenn ein Mensch die Sache bedient: ohne dass ein Mensch die Sache bedient.
Die vierzehn Releases sind im Postmortem zu den Kits ztl-next und ztl-start dokumentiert. Die Hook-Konfiguration stammt aus meiner eigenen ~/.claude/settings.json.
Baust du an etwas Ähnlichem?
Ich schreibe hier über Setups, die ich selbst benutze. Wenn du an etwas Vergleichbarem arbeitest, würde mich interessieren, wie dein Workflow aussieht.