When an AI agent can change both the code and the tests that accept it, a green build leaves a question open: did the code meet the agreed behavior, or did the acceptance criteria move to meet the code? At Hands-on Architects we call our answer the Behavioral Contract Gate: business requirements become an acceptance contract that people review and freeze before implementation starts, agents work against it, and only a person may change it. Below, one constructed feature goes through the gate, followed by its limits and a first pilot.
What the Behavioral Contract Gate does
The contract is a set of acceptance scenarios, written in plain language, each backed by an executable test. They describe the agreed behavior as someone outside the system observes it: requests, responses, state changes. We described the scenario format in our E2E test harness post.
The gate splits delivery into three phases, and each one ends with a decision that a named person makes:
- Requirements. Product and engineering agree the behavior.
- Contract. The team turns that agreement into scenarios and tests, checks why each test fails, or why it already passes, before the code exists, and freezes them.
- Implementation. Agents write the code against the frozen contract until it passes.
"Frozen" does not mean permanent: a change to the contract is a separate decision, made by a person and visible in review. That is the core of the idea: permission to implement a requirement is not permission to redefine how it is accepted.
None of the ingredients is new: acceptance tests written first, specification by example, red before green. What the gate adds is a rule for who may change the tests once agents write the code. In the diagram, code fixes stay inside the frozen contract, and contract changes go back through a person.
Step 1: from a business requirement to agreed behavior
The running example is constructed for this post; it does not come from a client project. A food-ordering app adds order cancellation. The product owner's request is one sentence: "A customer can cancel for free until the restaurant accepts the order, and gets a full refund."
The work in this phase is questions, not code. What happens after the restaurant accepts? What does a full refund mean when part of the order was paid with a voucher? What if the customer taps "cancel" twice? An agent can read the request against the existing system and list such gaps; the answers come from the product owner and go into a decision log. The team then writes the decisions back in business language, so the product owner confirms the wording, not only the intent.
The result is a short list of agreed rules:
- Before the restaurant accepts, the order is canceled and the customer gets a full refund to their card.
- After the restaurant accepts, the order stays as it is, and the customer is pointed to support.
- A second cancellation request changes nothing and issues no second refund.
Step 2: from agreed behavior to an acceptance contract
Each rule becomes one or more scenarios. A scenario is short enough for the product owner to read and precise enough to execute:
SCN-CANCEL-002: Cancellation after the restaurant accepted the order
Preconditions:
- A customer has a paid order
- The restaurant has accepted the order
Steps:
1. The customer requests cancellation.
POST /orders/{id}/cancel -> 409, reason=ALREADY_ACCEPTED, supportUrl present
2. The customer reads the order.
GET /orders/{id} -> 200, status=ACCEPTED
3. The customer reads the order's refunds.
GET /orders/{id}/refunds -> 200, empty list
Expected:
- The order is not canceled and no refund is issued
- The response points the customer to supportAgents can draft the scenarios and the tests behind them. A named person accepts, changes or rejects each scenario, and an acceptance covers only the exact text they read.
Then the team runs the new tests before any cancellation code exists. A scenario for behavior that does not exist yet must fail, and a reviewer reads why it fails. In our example, SCN-CANCEL-002 passed on the first run. The cancel endpoint did not exist yet, so the app answered with an error, and the first draft of the test only checked that the request was rejected. It passed for the wrong reason. The reviewer tightened the assertion to the agreed status, reason and support link. Now it failed because the agreed response was missing: the right reason.
A scenario that already passes is not always wrong: it may describe behavior the system has and must keep. A person decides which case it is and records the decision.
Step 3: freeze the contract
Once each scenario fails for the right reason or passes by a recorded decision, a person freezes the contract. Freezing records a fingerprint, a cryptographic hash of the contract files, in a freeze record committed next to them:
contract: order-cancellation
files: scenarios/order-cancellation.md, tests/acceptance/order_cancellation/
digest: sha256:9f2c41...d07e
frozen: 2026-09-21A matching fingerprint shows one thing: the files are byte for byte what the record describes. It is not the approval; the approval is the review before it.
Two controls hold the freeze:
- Prevention. The agents that implement the feature can read and run the contract, but their tools, the shell included, are denied writes to the contract files, the freeze record and the configuration that sets these rules. That also stops them re-freezing, because re-freezing writes the record.
- Detection. A CI step recomputes the fingerprint on every change and fails the build when it does not match the record. The failure asks for a decision, not a fix.
You need both. Prevention stops the configured agents from changing either the files or the record, and covers nothing else. Detection catches any edit the record does not match, whoever made it, but only after the fact, and it cannot see a change that updates the files and the record together. So make a contract owner a required reviewer of the freeze record, and keep that rule out of the agents' reach.
Protect the execution path too: a frozen test gives no evidence if it never runs, and a change to its helpers can weaken what it checks. Runner configuration, test selection, skip or allow lists, fixtures, stubs and the CI job running the fingerprint check belong inside the frozen set or under the same review.
Contracts do need to change. A person reviews the reason and the affected assertions, checks that missing behavior fails for the right reason, records why any changed scenario already passes, and re-freezes. The new fingerprint appears as a diff in the pull request, next to the reason for the change.
Step 4: implement against the frozen contract
A person starts implementation against named, frozen scenarios, and the agents run the contract as often as they like. A red test is information: diagnose it against the agreed behavior first. An agent may fix the code, but only a person decides that a test or the agreed rules are wrong:
- The code breaks an agreed behavior: fix the code.
- A test asserts something nobody agreed: repair it through the change route.
- The agreed rules are wrong or incomplete: change them first, then the contract.
Never weaken a correct expectation to get a green build.
Code review changes too. Reviewers spend their time on the design, the data model, code that passes the scenarios without implementing the rule (such as special cases for test data), and risks no scenario covers.
In our Harness Model, the frozen contract is a sensor in the work harness: a check that observes the agent's output and feeds the result back, in Birgitta Böckeler's sense. Within the permissions you enforce, the agent it checks cannot retune it.
A green contract can still be wrong
Back to the voucher question. In our constructed example, the product owner did answer it: the voucher part of a payment comes back as voucher credit, not money. The answer sat in the decision log, but the confirmed rule list the scenarios were drafted from said only "a full refund to their card". The scenarios covered card-only payments, and the code passed them. Human acceptance testing then found the mixed-payment case: the voucher part was refunded to the card too. The fix went in order: the agreed rules, a new scenario confirmed failing, a re-freeze, then the code.
Two limits follow, and the gate on its own addresses neither:
- The agreed rules may be incomplete or contradictory, as in the voucher case.
- The tests may misrepresent the rules or miss incorrect behavior: a test that passes for the wrong reason, code written to the tests rather than to the rule, or a stubbed dependency that behaves unlike the real one.
A passing suite is evidence about the behaviors its assertions cover. People still need to challenge the requirements, review the tests and check what the contract does not reach.
Try it on one bounded feature
A suggested first pilot, not a measured recipe:
- Choose a feature with clear boundaries. Name the behavior, its dependencies and what is out of scope.
- Name the owners. Decide who answers domain questions, who reviews the scenarios and their assertions, who approves contract changes and who accepts the result.
- Build the contract before the code. Confirm that each scenario for new behavior fails for the right reason, record a decision for each one that already passes, and freeze.
- Protect it and measure. Put the controls from Step 3 in place, the required reviewer for the freeze record included, and keep human acceptance testing. Record the contract changes, the defects the contract missed and the time the contract takes to build and maintain; we have no general numbers for that overhead yet.
The first step is small: agree the behavior, and agree who may change how it is accepted.