Der Begriff Harness Engineering hat sich in den letzten Monaten etabliert. Die Formel dahinter ist einfach: Agent = Modell + Harness. Der Harness ist alles, was einen KI-Agenten ausmacht, ausser dem Modell selbst. Dazu gehören Instruktionsdateien wie CLAUDE.md oder AGENTS.md, Skills, Tools, Tests, Linter und CI-Gates.

Der Kernpunkt von Harness Engineering ist eine Verschiebung des Fokus. Wir hören auf, dem Agenten im Prompt zu sagen, was er tun soll, und beginnen, das System rund um den Agenten so zu gestalten, dass er gar nicht anders kann. Ein Satz wie „halte dich an unsere Architektur“ im Prompt ist eine Hoffnung. Ein Test, der den Build bricht, ist eine Garantie.

Im AI Unified Process ist der Harness eine der drei Schichten. In diesem Beitrag zeige ich anhand des Projekts aiup-petclinic, wie die Schichten zusammenspielen, warum das CLAUDE.md dort bewusst klein ist und warum das Software Architektur Dokument die eigentliche Arbeit macht.

Die drei Schichten des AI Unified Process

Der AI Unified Process unterscheidet drei Schichten:

What: Was soll das System tun? Hier leben die Requirements, das Use Case Diagramm, die Use Case Spezifikationen und das Entity Model. Diese Artefakte sind technologieneutral. Sie beschreiben das Verhalten aus Sicht der Anwender und des Business.

Harness: Wie stellen wir sicher, dass der Agent das Richtige richtig baut? Hier leben die Instruktionsdateien, die Skills, die Test-Strategie, die CI-Gates und das Software Architektur Dokument.

How: Der eigentliche Code, die Migrationen, die Tests. Das ist der Output des Agenten.

Die Reihenfolge ist wichtig. Ohne What weiss der Agent nicht, was er bauen soll. Ohne Harness baut er es auf eine Art, die zwar funktioniert, aber nicht zu unserem System passt.

Grössere Instruktionsdateien sind nicht besser

Die verbreitete Annahme ist: Je mehr Kontext der Agent hat, desto besser arbeitet er. Also wandert alles ins CLAUDE.md: Projektbeschreibung, Verzeichnisbaum, Architektur, Coding-Konventionen, Testregeln, Befehle.

Eine Studie der ETH Zürich vom Februar 2026 (Gloaguen, Mündler, Müller, Raychev, Vechev: „Evaluating AGENTS.md“) hat das empirisch geprüft. Die Ergebnisse sind ernüchternd. Automatisch generierte Kontextdateien haben die Erfolgsrate der Agenten im Schnitt um 3 Prozent gesenkt und die Kosten um über 20 Prozent erhöht. Von Menschen geschriebene Dateien haben nur etwa 4 Prozent gebracht, bei ähnlich höheren Kosten. Und die Agenten haben die Anweisungen sehr wohl befolgt. Das Problem war nicht mangelnder Gehorsam, sondern dass die Anweisungen Rauschen, redundante Schritte und unnötige Einschränkungen hinzugefügt haben.

Interessant ist ein Detail der Studie: Wenn die Forscher die bestehende Dokumentation aus den Repositories entfernt haben, haben die Kontextdateien plötzlich geholfen. Der Schluss daraus: Eine Kontextdatei bringt etwas, wenn sie Wissen liefert, das der Agent sonst nirgends findet. Sie bringt nichts, wenn sie wiederholt, was schon in der Dokumentation steht, und sie schadet, wenn sie den Kontext mit Ballast füllt.

Für Harness Engineering heisst das: Das CLAUDE.md ist keine Wissensdatenbank. Es ist eine Landkarte.

Das CLAUDE.md im Petclinic-Projekt

Das CLAUDE.md in aiup-petclinic ist rund 150 Zeilen lang und enthält im Wesentlichen vier Dinge:

  1. Was die Quelle der Wahrheit ist. docs/ ist die Quelle der Wahrheit, nicht der Code. Wenn Use Case und Code sich widersprechen, gewinnt der Use Case. Und welche Sensors diese Behauptung prüfen.
  2. Den Stack und die Befehle. Java 25, Spring Boot 4.1, Vaadin 25.2, jOOQ 3.21, Flyway, PostgreSQL. Wie man baut, testet und jOOQ-Klassen neu generiert. Das ist Wissen, das der Agent nirgends anders findet.
  3. Wann welches Dokument zu lesen ist. Vor der Implementation eines Use Case: die Use Case Spezifikation. Vor jeder Änderung in src/main/javadocs/guidelines/architecture.md. Vor jedem Test: docs/guidelines/testing.md.
  4. Welche Skills es gibt. Die Skills des AI Unified Process werden aufgezählt, damit der Agent sie einer Ad-hoc-Generierung vorzieht.

Was nicht im CLAUDE.md steht: die Architekturregeln selbst. Kein Package-Layout, keine jOOQ-Patterns, keine Vaadin-Konventionen. Die stehen im Software Architektur Dokument, und der Agent liest sie nur dann, wenn er sie braucht.

Das ist der Unterschied zwischen einem Kontext, der immer geladen ist, und einem Kontext, der bei Bedarf geladen wird. Das CLAUDE.md wird bei jeder Session in den Kontext geladen. Das Architektur Dokument nur, wenn der Agent Code schreibt. Beim Verfassen einer Use Case Spezifikation belastet es den Kontext nicht.

Das muss man aktiv verteidigen. Als die beiden Traceability-Sensors dazukamen (dazu später mehr), wuchs das CLAUDE.md um einen Absatz, der erklärte, was jeder Sensor prüft. Der Absatz war korrekt, aber er wiederholte, was in testing.md ausführlicher steht. Im nächsten Commit war er auf drei Sätze und einen Link gekürzt: Es gibt zwei Sensors, die Status:-Zeile ist deshalb eine Behauptung und kein Etikett, und was jeder Sensor prüft, steht in testing.md. Die Landkarte zeigt, wo das Wissen liegt. Sie ist nicht das Wissen.

Das Software Architektur Dokument als Bindeglied

Im Rational Unified Process war das Software Architektur Dokument das zentrale Dokument der Elaboration-Phase. Viele Teams haben es in den letzten Jahren abgeschafft. Es war zu aufwändig, es wurde nicht gelesen, es war nach drei Monaten veraltet.

Mit KI-Agenten ändert sich die Rechnung. Der Agent liest das Dokument, bei jedem Task, der Code berührt. Und er hält sich daran, wenn es kurz und konkret ist.

Im Petclinic-Projekt ist das Architektur Dokument docs/guidelines/architecture.md, rund 100 Zeilen. Es beantwortet genau die Fragen, die der Agent sonst mit dem beantworten würde, was er im Training gesehen hat:

  • Package-Struktur: Package-by-Feature unter ai.unifiedprocess.petclinic. Jedes Feature (ownerpetvisitvet) hat die Sub-Packages ui und domain. Kein separater Service-Layer, kein DTO-Layer, solange ein Use Case es nicht verlangt.
  • Datenzugriff: jOOQ, kein JPA, keine Spring Data Repositories. Records werden mit Records.mapping(Type::new) gemappt, nie mit fetchInto. Parent-Child-Beziehungen mit multiset, um N+1 zu vermeiden.
  • Persistence-Stereotyp: Klassen heissen <Entity>Repository und sind mit @Repository annotiert, damit die Exception-Translation von Spring greift.
  • Vaadin-Konventionen: Eine View pro Use Case. Styling nur über LumoUtility, nie über getStyle().set(). Navigation mit SideNav. Validierung im Formular, nicht im Domain-Record.
  • Cross-Feature-Regel: Zugriff auf ein anderes Feature nur über dessen domain-Package, mit einer klar definierten Ausnahme für Routing-Tokens.

Ohne dieses Dokument hätte der Agent wahrscheinlich OwnerControllerOwnerServiceOwnerRepository und OwnerDTO gebaut. Technisch korrekt, aber eine andere Architektur, und nach zwanzig Use Cases ein Projekt, das niemand mehr versteht.

Vom Dokument zum Harness: Guides und Sensors

Ein Architektur Dokument allein ist noch kein Harness. Es wird erst zum Harness, wenn wir es in zwei Formen operationalisieren.

Guides: Das Dokument steuert den Agenten

Guides leiten den Agenten vor der Arbeit. Im Petclinic-Projekt gibt es zwei Ebenen:

  • Das CLAUDE.md verweist auf das Architektur Dokument und sagt, wann es zu lesen ist.
  • Der Skill aiup-vaadin-jooq:implement kennt das Architektur Dokument bereits und setzt die Konventionen beim Implementieren um.

Der Skill ist im Grunde ein ausführbarer Ausschnitt aus dem Architektur Dokument. Er sagt nicht nur, dass es ein ui– und ein domain-Package gibt, sondern zeigt, wie eine View aussieht, wie ein Repository aussieht und wie beide getestet werden.

Sensors: Das System prüft den Agenten

Sensors prüfen nach der Arbeit, ob der Agent sich an die Regeln gehalten hat. Hier wird das Architektur Dokument zu etwas, das den Build bricht.

Der wichtigste Sensor im Petclinic-Projekt ist ArchitectureTest, ein ArchUnit-Test im Root-Package. Er ist die ausführbare Version von architecture.md. Jede Regel nennt in ihrer because-Klausel den Abschnitt, aus dem sie stammt, damit ein Fehler direkt auf das Dokument zurückzeigt:

@AnalyzeClasses(
        packages = "ai.unifiedprocess.petclinic",
        importOptions = ImportOption.DoNotIncludeTests.class)
class ArchitectureTest {

    @ArchTest
    static final ArchRule featuresHaveOnlyUiAndDomain =
            classes()
                    .that().resideInAPackage("ai.unifiedprocess.petclinic.(*)..")
                    .and().resideOutsideOfPackage("..core..")
                    .should().resideInAnyPackage("..ui..", "..domain..")
                    .because("architecture.md: each feature has exactly the sub-packages ui and domain");

    @ArchTest
    static final ArchRule noJpaNoSpringData =
            noClasses()
                    .should().dependOnClassesThat()
                    .resideInAnyPackage("jakarta.persistence..", "org.springframework.data..")
                    .because("architecture.md: jOOQ only, no JPA, no Spring Data");

    @ArchTest
    static final ArchRule noFetchInto =
            noClasses()
                    .should().callMethodWhere(
                            JavaCall.Predicates.target(HasName.Predicates.name("fetchInto")))
                    .because("architecture.md: use Records.mapping(Type::new) for compile-time column checking");

    @ArchTest
    static final ArchRule repositoriesAreNamedAndAnnotated =
            classes()
                    .that().haveSimpleNameEndingWith("Repository")
                    .should().beAnnotatedWith(Repository.class)
                    .andShould().resideInAPackage("..domain..")
                    .because("architecture.md: @Repository enables exception translation");

    // ... weitere Regeln für Domain-Records, Views und Styling
}

Der Test deckt die Package-Struktur, den Verzicht auf Service- und DTO-Layer, die Domain-als-Grenze-Regel, das jOOQ-Mapping, den Persistence-Stereotyp und die Vaadin-Konventionen ab. Und das Dokument verweist zurück auf den Test. Der erste Absatz in architecture.md sagt: Das meiste hier wird durch ArchitectureTest durchgesetzt. Wer eine Konvention ändert, ändert die Regel im selben Commit. Eine Regel, die dem Dokument widerspricht, ist die Regel, die falsch ist.

Damit ist das Paar geschlossen. Das Dokument ist die lesbare Form, der Test die ausführbare. Der Agent liest beide, und der Build prüft, dass sie übereinstimmen.

Beim Schreiben des Tests sind zwei Dinge passiert, die den Wert dieses Ansatzes zeigen:

  • Die Regel „keine Zyklen zwischen Features“ war als Prosa plausibel, als Test aber falsch. Das Dokument erlaubt ausdrücklich, dass die ui eines Features in die domain eines anderen greift und fremde Views als Routing-Token referenziert. Auf Feature-Ebene ist der Graph damit absichtlich zyklisch. Der Test prüft deshalb nur die domain-Packages auf Zyklen. Das Dokument war unpräzise, und erst der Test hat es sichtbar gemacht.
  • Die Regel „kein getStyle().set()“ liess sich nicht mit callMethod(HasStyle.class, "getStyle") prüfen, weil ArchUnit als Ziel den konkreten Komponententyp sieht, nicht das Interface. Die Regel braucht ein Prädikat auf assignableTo(HasStyle.class). Ein Detail, das man nur beim Ausführen lernt.

Zwei Sensors auf der What-Schicht

ArchUnit prüft die How-Schicht: Ist der Code so gebaut, wie das Architektur Dokument es verlangt? Aber der AI Unified Process behauptet mehr. Er sagt, docs/ sei die Quelle der Wahrheit. Diese Behauptung war lange nicht prüfbar. Ein Use Case konnte auf Status: Done stehen, während ein Alternative Flow oder eine Business Rule nie getestet wurde. Ein Test Case konnte auf Status: Automated stehen, ohne dass ein Journey-Test existierte. Und wenn jemand einen Flow in der Spezifikation umbenannt hat, zeigte die Annotation im Test stillschweigend ins Leere.

Zwei Tests schliessen diese Lücke, einer pro Dokumentfamilie.

UseCaseTraceabilityTest liest jede docs/use_cases/UC-*.md und jede @UseCase-Annotation auf dem Test-Classpath und vergleicht beides in zwei Richtungen:

  • Referentielle Integrität, für jeden Use Case, unabhängig vom Status. Jede Annotation muss auf etwas zeigen, das existiert: die id auf eine Spezifikationsdatei, das scenario auf eine ### A1: ...-Überschrift, jede businessRules-Angabe auf eine ### BR-NNN: ...-Überschrift. Wer einen Alternative Flow in der Spezifikation umbenennt, bricht den Build, bis die Annotationen nachziehen.
  • Abdeckung, nur für Use Cases mit Status: Done oder Tested. Ein solcher Use Case braucht einen Test für das Main Success Scenario, für jeden Alternative Flow und für jede Business Rule. Alle anderen Status sind ausgenommen, denn das Projekt schreibt die Spezifikation vor dem Code, und ein noch nicht implementierter Use Case ist ein normaler Zwischenstand, kein Fehler.
@Test
void completedUseCasesCoverEveryAlternativeFlow() {
    List<String> violations = new ArrayList<>();
    completedSpecifications().forEach(spec -> {
        Set<String> tested = testedScenarios(spec.id()).collect(toSet());
        spec.alternativeFlows().stream()
                .filter(flow -> !tested.contains(flow))
                .forEach(flow -> violations.add(spec.id() + " is '" + spec.status()
                        + "' but alternative flow \"" + flow + "\" has no test."
                        + " Annotate one @UseCase(id = \"" + spec.id()
                        + "\", scenario = \"" + flow + "\")"));
    });
    assertNoViolations("Alternative flows of completed use cases without a test", violations.stream());
}

TestCaseTraceabilityTest macht dasselbe für docs/test_cases/TC-*.md. Ein Test Case ist eine User Journey über mehrere Use Cases, im Petclinic-Projekt zum Beispiel TC-001: Besitzer registrieren, wiederfinden, Details prüfen, Tier hinzufügen, Besuch buchen. Der Journey-Test TC001NewOwnerFirstVisitIT trägt eine @TestCase-Annotation auf der Klasse:

@TestCase(id = "TC-001", useCases = {"UC-003", "UC-004", "UC-005", "UC-007", "UC-009"})
class TC001NewOwnerFirstVisitIT extends AbstractBasePlaywrightIT {

Der Sensor prüft, dass ein Test Case mit Status: Automated eine TC<NNN><Name>IT-Klasse hat und umgekehrt jede solche Klasse ein Dokument, dass die id mit dem Klassennamen übereinstimmt, und vor allem: dass die Liste in useCases exakt die Use Cases nennt, die die Flow-Tabelle des Dokuments verlinkt, in beide Richtungen. Die Liste ist das Einzige, was der Klassenname nicht ausdrücken kann, und deshalb das Einzige, was sich lohnt zu prüfen.

Dazu kommt eine Regel über die Schichten hinweg: Ein automatisierter Test Case darf nur durch Use Cases laufen, die selbst Done oder Tested sind. Ein grüner End-to-End-Test über einen Use Case im Status Draft heisst, dass eine der beiden Statuszeilen lügt.

Die Konsequenz steht so in testing.md: Die Status:-Zeile ist eine Behauptung, kein Etikett. Wer sie auf Done oder Automated setzt, schaltet den Sensor ein. Genau deshalb darf sie nicht von Hand gesetzt werden, ohne vorher coverage-check laufen zu lassen.

Drei Details an den beiden Tests finde ich bemerkenswert:

  • Jede Annotation gehört an einen Ort. @UseCase sitzt auf der Methode, weil ein Use Case viele Abdeckungseinheiten hat (Main Scenario, jeder Flow, jede Business Rule) und nur der Autor weiss, welche Methode welche Einheit abdeckt. @TestCase sitzt auf der Klasse, weil ein Test Case genau eine Einheit hat: die Journey. Beide Sensors weisen ihre Annotation an jedem anderen Ort zurück. Ohne diese Regel könnte ein Journey-Test stillschweigend einen der fünf Use Cases „abdecken“, die er durchläuft, und das sähe nach Abdeckung aus, während die Alternative Flows dieses Use Case ungetestet bleiben.
  • Ein Guard-Test prüft, dass überhaupt Spezifikationen und Annotationen gefunden wurden. Ohne ihn würde ein falsches Arbeitsverzeichnis alle Checks über eine leere Menge grün laufen lassen, ein Sensor, der grün meldet, weil er blind ist.
  • Die Tests melden alle Verstösse auf einmal, nicht nur den ersten. Der Fehlerbericht ist damit eine Arbeitsliste, und zwar eine, die der Agent direkt abarbeiten kann.

Die weiteren Sensors im Projekt:

  • jOOQ generiert Code aus dem Schema. Ein falscher Spaltenname bricht die Kompilierung. Die Flyway-Migration ist damit die Schema-DSL, und das Entity Model muss dazu passen.
  • Browserless-Tests (UC<NNN><Name>Test) prüfen jede View gegen die Use Case Spezifikation. Jede Testmethode trägt eine @UseCase-Annotation mit ID, Szenario und Business Rules. Das ist der Input für UseCaseTraceabilityTest.
  • Playwright-Tests (TC<NNN><Name>IT) prüfen ganze User Journeys im Browser. Jede Klasse trägt eine @TestCase-Annotation. Das ist der Input für TestCaseTraceabilityTest.
  • Die Namenskonvention entscheidet über die Build-Phase: *Test läuft in test*IT in verify. Ein falsch benannter Test wird nicht ausgeführt und fällt auf.

Der Unterschied zwischen Guide und Sensor ist der Unterschied zwischen probabilistischer und deterministischer Einhaltung. Ein Guide erhöht die Wahrscheinlichkeit, dass der Agent richtig arbeitet. Ein Sensor stellt sicher, dass falsches Arbeiten nicht durchkommt. Ein gutes Harness braucht beides.

Was das für die Praxis bedeutet

Das CLAUDE.md ist eine Landkarte, keine Bibliothek. Es enthält, was der Agent sonst nirgends findet: die Quelle der Wahrheit, den Stack, die Befehle, und wann welches Dokument zu lesen ist. Alles andere gehört in eigene Dokumente, die bei Bedarf gelesen werden. Die ETH-Studie zeigt, dass mehr Kontext nicht mehr Qualität bringt, sondern mehr Kosten. Und das CLAUDE.md wächst von allein, wenn man es nicht regelmässig zurückschneidet.

Das Software Architektur Dokument ist wieder ein First-Class-Artefakt. Kurz, konkret, mit Codebeispielen. Es beantwortet die Fragen, die der Agent sonst mit generischen Antworten aus dem Training füllt.

Sensors gehören auf beide Schichten. ArchUnit prüft, dass der Code zur Architektur passt. Die beiden Traceability-Tests prüfen, dass die Tests zur Spezifikation passen, für Use Cases und für Test Cases. Erst mit beiden ist die Behauptung „docs ist die Quelle der Wahrheit“ mehr als ein Satz im CLAUDE.md.

Architekturentscheidungen sollten als Sensor umgesetzt werden. Jede Regel, die man mit Compiler, ArchUnit oder Test prüfen kann, sollte man prüfen. Und das Dokument sollte auf den Test verweisen und der Test auf das Dokument. Alles andere bleibt eine Hoffnung.

Harness Engineering ist Architekturarbeit. Die Rolle des Architekten verschiebt sich vom Code-Reviewer zum Designer des Systems, in dem der Agent arbeitet. Wer die Architektur klar beschreibt und durchsetzt, bekommt Code, der ins System passt.

Der AI Unified Process macht diese Verbindung explizit. Die What-Schicht sagt, was gebaut wird. Das Architektur Dokument in der Harness-Schicht sagt, wie es gebaut wird. Und die Sensors stellen sicher, dass es auch so gebaut wurde.