The term Harness Engineering has established itself in recent months. The formula behind it is simple: Agent = Model + Harness. The harness is everything that makes up an AI agent, except the model itself. That includes instruction files like CLAUDE.md or AGENTS.md, skills, tools, tests, linters and CI gates.
The core point of Harness Engineering is a shift of focus. We stop telling the agent in the prompt what to do, and we start designing the system around the agent so that it cannot do otherwise. A sentence like “follow our architecture” in the prompt is a hope. A test that breaks the build is a guarantee.
In the AI Unified Process, the harness is one of three layers. In this post I use the aiup-petclinic project to show how the layers work together, why the CLAUDE.md there is deliberately small, and why the Software Architecture Document does the real work.
The Three Layers of the AI Unified Process #
The AI Unified Process distinguishes three layers:
What: What should the system do? This is where the requirements, the use case diagram, the use case specifications and the entity model live. These artifacts are technology-neutral. They describe behavior from the point of view of the users and the business.
Harness: How do we make sure the agent builds the right thing the right way? This is where the instruction files, the skills, the test strategy, the CI gates and the Software Architecture Document live.
How: The actual code, the migrations, the tests. This is the output of the agent.
The order matters. Without What, the agent does not know what to build. Without Harness, it builds it in a way that works but does not fit our system.
Bigger Instruction Files Are Not Better #
The common assumption is: the more context the agent has, the better it works. So everything ends up in the CLAUDE.md: project description, directory tree, architecture, coding conventions, test rules, commands.
A study from ETH Zurich from February 2026 (Gloaguen, Mündler, Müller, Raychev, Vechev: “Evaluating AGENTS.md”) tested this empirically. The results are sobering. Automatically generated context files lowered the success rate of the agents by 3 percent on average and raised the cost by over 20 percent. Human-written files brought only about 4 percent, with a similar increase in cost. And the agents did follow the instructions. The problem was not a lack of obedience, but that the instructions added noise, redundant steps and unnecessary constraints.
One detail of the study is interesting: when the researchers removed the existing documentation from the repositories, the context files suddenly helped. The conclusion: a context file is useful when it provides knowledge the agent cannot find anywhere else. It is useless when it repeats what is already in the documentation, and it is harmful when it fills the context with ballast.
For Harness Engineering this means: the CLAUDE.md is not a knowledge base. It is a map.
The CLAUDE.md in the PetClinic Project #
The CLAUDE.md in aiup-petclinic is about 150 lines long and contains essentially four things:
- What the source of truth is.
docs/is the source of truth, not the code. If a use case and the code disagree, the use case wins. And which sensors check this claim. - The stack and the commands. Java 25, Spring Boot 4.1, Vaadin 25.2, jOOQ 3.21, Flyway, PostgreSQL. How to build, test and regenerate the jOOQ classes. This is knowledge the agent cannot find anywhere else.
- When to read which document. Before implementing a use case: the use case specification. Before any change in
src/main/java:docs/guidelines/architecture.md. Before any test:docs/guidelines/testing.md. - Which skills exist. The skills of the AI Unified Process are listed so the agent prefers them over ad-hoc generation.
What is not in the CLAUDE.md: the architecture rules themselves. No package layout, no jOOQ patterns, no Vaadin conventions. Those are in the Software Architecture Document, and the agent reads them only when it needs them.
This is the difference between context that is always loaded and context that is loaded on demand. The CLAUDE.md is loaded into the context in every session. The architecture document only when the agent writes code. When writing a use case specification, it does not burden the context.
This has to be defended actively. When the two traceability sensors were added (more on those later), the CLAUDE.md grew by a paragraph explaining what each sensor checks. The paragraph was correct, but it repeated what testing.md says in more detail. In the next commit it was cut down to three sentences and a link: there are two sensors, the Status: line is therefore an assertion and not a label, and what each sensor checks is in testing.md. The map shows where the knowledge is. It is not the knowledge.
The Software Architecture Document as the Link #
In the Rational Unified Process, the Software Architecture Document was the central document of the Elaboration phase. Many teams have dropped it in recent years. It was too much effort, nobody read it, it was out of date after three months.
With AI agents the math changes. The agent reads the document, on every task that touches code. And it follows it, as long as the document is short and concrete.
In the PetClinic project, the architecture document is docs/guidelines/architecture.md, about 100 lines. It answers exactly the questions the agent would otherwise answer with what it has seen in training:
- Package structure: package-by-feature under
ai.unifiedprocess.petclinic. Each feature (owner,pet,visit,vet) has the sub-packagesuianddomain. No separate service layer, no DTO layer, unless a use case demands it. - Data access: jOOQ, no JPA, no Spring Data repositories. Records are mapped with
Records.mapping(Type::new), never withfetchInto. Parent-child relationships usemultisetto avoid N+1. - Persistence stereotype: classes are named
<Entity>Repositoryand annotated with@Repository, so Spring’s exception translation applies. - Vaadin conventions: one view per use case. Styling only via
LumoUtility, never viagetStyle().set(). Navigation withSideNav. Validation in the form, not in the domain record. - Cross-feature rule: access to another feature only through its
domainpackage, with one clearly defined exception for routing tokens.
Without this document, the agent would probably have built OwnerController, OwnerService, OwnerRepository and OwnerDTO. Technically correct, but a different architecture, and after twenty use cases a project nobody understands anymore.
From Document to Harness: Guides and Sensors #
An architecture document alone is not yet a harness. It becomes a harness only when we operationalize it in two forms.
Guides: The Document Steers the Agent
Guides direct the agent before the work. In the PetClinic project there are two levels:
- The CLAUDE.md points to the architecture document and says when to read it.
- The skill
aiup-vaadin-jooq:implementalready knows the architecture document and applies the conventions during implementation.
The skill is essentially an executable excerpt of the architecture document. It does not just say that there is a ui and a domain package. It shows what a view looks like, what a repository looks like and how both are tested.
Sensors: The System Checks the Agent
Sensors check after the work whether the agent followed the rules. This is where the architecture document becomes something that breaks the build.
The most important sensor in the PetClinic project is ArchitectureTest, an ArchUnit test in the root package. It is the executable version of architecture.md. Every rule names in its because clause the section it comes from, so a failure points straight back to the document:
@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");
// ... further rules for domain records, views and styling
}
The test covers the package structure, the absence of service and DTO layers, the domain-as-boundary rule, the jOOQ mapping, the persistence stereotype and the Vaadin conventions. And the document points back to the test. The first paragraph of architecture.md says: most of what follows is enforced by ArchitectureTest. If you change a convention, change the rule in the same commit. A rule that contradicts the document is the rule that is wrong.
That closes the pair. The document is the readable form, the test the executable one. The agent reads both, and the build checks that they agree.
While writing the test, two things happened that show the value of this approach:
- The rule “no cycles between features” was plausible as prose but wrong as a test. The document explicitly allows the
uiof one feature to reach into thedomainof another and to reference other views as routing tokens. At the feature level, the graph is therefore cyclic by design. The test only checks thedomainpackages for cycles. The document was imprecise, and only the test made that visible. - The rule “no
getStyle().set()” could not be checked withcallMethod(HasStyle.class, "getStyle"), because ArchUnit sees the concrete component type as the target, not the interface. The rule needs a predicate onassignableTo(HasStyle.class). A detail you only learn by running it.
Two Sensors on the What Layer
ArchUnit checks the How layer: is the code built the way the architecture document demands? But the AI Unified Process claims more. It says docs/ is the source of truth. For a long time, that claim was not checkable. A use case could be Status: Done while an alternative flow or a business rule was never tested. A test case could be Status: Automated without a journey test behind it. And when someone renamed a flow in the specification, the annotation in the test silently pointed at nothing.
Two tests close this gap, one per document family.
UseCaseTraceabilityTest reads every docs/use_cases/UC-*.md and every @UseCase annotation on the test classpath and compares the two in both directions:
- Referential integrity , for every use case, regardless of status. Every annotation must point at something that exists: the
idat a specification file, thescenarioat a### A1: ...heading, everybusinessRulesentry at a### BR-NNN: ...heading. Renaming an alternative flow in the specification breaks the build until the annotations follow. - Coverage , only for use cases with
Status: DoneorTested. Such a use case needs a test for the main success scenario, for every alternative flow and for every business rule. All other statuses are exempt, because the project writes the specification before the code, and a use case that is not yet implemented is a normal intermediate state, not a defect.
@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 does the same for docs/test_cases/TC-*.md. A test case is a user journey across several use cases, in the PetClinic project for example TC-001: register an owner, find them again, review the details, add a pet, book a visit. The journey test TC001NewOwnerFirstVisitIT carries a @TestCase annotation on the class:
@TestCase(id = "TC-001", useCases = {"UC-003", "UC-004", "UC-005", "UC-007", "UC-009"})
class TC001NewOwnerFirstVisitIT extends AbstractBasePlaywrightIT {
The sensor checks that a test case with Status: Automated has a TC<NNN><Name>IT class and, the other way round, that every such class has a document, that the id agrees with the class name, and above all: that the list in useCases names exactly the use cases the Flow table of the document links, in both directions. The list is the one thing the class name cannot express, and therefore the one thing worth checking.
On top of that there is a rule across the layers: an automated test case may only walk through use cases that are themselves Done or Tested. A green end-to-end test over a use case in status Draft means one of the two status lines is lying.
The consequence is stated in testing.md: the Status: line is an assertion, not a label. Setting it to Done or Automated switches the sensor on. That is exactly why it must not be set by hand without running coverage-check first.
Three details of the two tests are worth noting:
- Every annotation has one place.
@UseCasesits on the method, because a use case has many coverage units (main scenario, each flow, each business rule) and only the author knows which method covers which unit.@TestCasesits on the class, because a test case has exactly one unit: the journey. Both sensors reject their annotation anywhere else. Without this rule, a journey test could silently “cover” one of the five use cases it walks through, which would look like coverage while the alternative flows of that use case stay untested. - A guard test checks that specifications and annotations were found at all. Without it, a wrong working directory would let every check pass over an empty set, a sensor that reports green because it is blind.
- The tests report every violation at once, not just the first. The failure report is therefore a work list, and one the agent can work through directly.
The other sensors in the project:
- jOOQ generates code from the schema. A wrong column name breaks compilation. The Flyway migration is therefore the schema DSL, and the entity model must match it.
- Browserless tests (
UC<NNN><Name>Test) check every view against the use case specification. Every test method carries a@UseCaseannotation with id, scenario and business rules. That is the input forUseCaseTraceabilityTest. - Playwright tests (
TC<NNN><Name>IT) check whole user journeys in the browser. Every class carries a@TestCaseannotation. That is the input forTestCaseTraceabilityTest. - The naming convention decides the build phase:
*Testruns intest,*ITinverify. A wrongly named test is not executed and gets noticed.
The difference between guide and sensor is the difference between probabilistic and deterministic compliance. A guide raises the probability that the agent works correctly. A sensor makes sure that incorrect work does not get through. A good harness needs both.
What This Means in Practice #
The CLAUDE.md is a map, not a library. It contains what the agent cannot find anywhere else: the source of truth, the stack, the commands, and when to read which document. Everything else belongs in separate documents that are read on demand. The ETH study shows that more context does not bring more quality, only more cost. And the CLAUDE.md grows on its own if you do not trim it regularly.
The Software Architecture Document is a first-class artifact again. Short, concrete, with code examples. It answers the questions the agent would otherwise fill with generic answers from training.
Sensors belong on both layers. ArchUnit checks that the code fits the architecture. The two traceability tests check that the tests fit the specification, for use cases and for test cases. Only with both is the claim “docs is the source of truth” more than a sentence in the CLAUDE.md.
Architecture decisions should be implemented as sensors. Every rule that can be checked with the compiler, ArchUnit or a test should be checked. And the document should point to the test, and the test to the document. Everything else remains a hope.
Harness Engineering is architecture work. The role of the architect shifts from code reviewer to designer of the system in which the agent works. Whoever describes and enforces the architecture clearly gets code that fits the system.
The AI Unified Process makes this connection explicit. The What layer says what is built. The architecture document in the Harness layer says how it is built. And the sensors make sure it was built that way.