Turning a Spec Into Guardrails for an AI Coding Agent
A spec file can say, in bold, that the order service must never charge a customer for an item that is out of stock. An AI coding agent can read that line, agree with it, and still write a checkout path that charges a backordered order, because the line was one instruction among thousands of tokens and nothing stopped the code from compiling. Scala spec-driven development closes that gap by treating each requirement as something to enforce rather than something to mention, so the agent's output fails the build the moment it breaks the spec.
Most teams adopting spec-driven development stop at the document. The spec gets versioned, the agent gets pointed at it, and everyone trusts the model to pay attention. A spec only constrains an AI coding agent once each line has been converted into a check the agent can't talk its way past, whether that check is a type that won't compile, a property that fails, or a lint rule that blocks the merge.
An AI coding agent treats a markdown spec as a suggestion. A spec becomes a guardrail when each requirement turns into a machine check that rejects bad output: refined types for value rules, one type per state for while requirements, exhaustive error types for failure cases, ScalaCheck properties for event rules, and module boundaries plus lint rules for scope. Lock those checks so the agent can't edit them, feed every failure back as the next prompt, and keep a short list of requirements that still need a human reviewer.
Why a Written Spec Doesn't Constrain an AI Coding Agent
An AI coding agent reads a spec the same way it reads everything else in its context window, as text that shifts the odds of what it writes next. A requirement in a markdown file raises the chance that the agent honors it, but a requirement in a markdown file can't guarantee it. Run the same agent on the same spec five times and you can get five different implementations, and some of them may quietly skip a rule.
Some advocates of spec-driven development answer that problem by promising determinism through better prompting. Better prompting helps at the margins, yet no amount of prompt craft turns a probabilistic generator into a predictable one. The realistic goal is a predictable gate around an unpredictable generator. Five different implementations of the same requirement are fine, as long as every implementation that reaches the main branch has passed the same checks.
Without that gate, enforcement falls back to code review. Code review is slow, it gets slower as agents produce more code, and reviewers facing a growing stream of generated code are under pressure to approve on trust. For an engineering leader, a review-only control means the spec's real enforcement rate equals the attention span of whoever reviews the pull request on a busy afternoon.
The Three Properties Every AI Agent Guardrail Needs
A spec line becomes a guardrail when the check behind it has three properties. A check that misses any one of them slides back into being advice.
- A machine can evaluate it. The check returns pass or fail without a person reading the code. Compiler errors, failing tests, and lint violations qualify, while "keep the domain logic clean" does not.
- It runs on every generation. The check fires on each agent attempt, locally and in CI, so a violation shows up in seconds instead of in a review queue days later.
- The agent can't edit it. The check lives in files the agent has no permission to change. A guardrail the agent can loosen is just a suggestion with extra steps.
How Scala Spec-Driven Development Maps Each Requirement to a Check
Scala spec-driven development has an advantage most stacks lack, because the language gives you a place to put almost every kind of requirement. A team that writes requirements in EARS, the five-pattern syntax covered in our breakdown of why spec-driven development doesn't fix the requirements problem, can map each pattern to a specific Scala mechanism. Scope limits, written as ubiquitous "shall not" statements, get their own row in the table because they need a different kind of enforcement.
| Requirement type | Example spec line | Scala guardrail |
|---|---|---|
| Ubiquitous (shall) | The order service shall accept line quantities from 1 to 500. | Refined type that makes an invalid value impossible to construct |
| State-driven (while) | While an order is backordered, the order service shall not capture payment. | One type per state, with functions that only accept valid states |
| Unwanted behavior (if, then) | If the warehouse reports a shortfall, then the order service shall backorder the order. | Closed error type with exhaustive matching under -Werror |
| Event-driven (when) | When a customer cancels an order, the order service shall release every reserved unit. | ScalaCheck property written before the implementation |
| Optional feature (where) | Where gift wrapping is enabled, the order service shall add a wrap fee. | Feature passed as a required parameter, so code that uses it can't compile without it |
| Scope limit (ubiquitous, shall not) | The domain module shall not depend on the HTTP or database layers. | sbt module boundaries plus Scalafix or WartRemover rules |
The following examples follow one hypothetical order service through the most common EARS patterns, showing how a single spec turns into several layers of Scala enforcement.
How Refined Types Enforce Value Rules in a Spec
Ubiquitous requirements state rules that always hold, and many of them are rules about values such as quantities, ranges, formats, and identifiers. A refined type library like Iron attaches those rules to the type itself, so a value that breaks the rule can't exist anywhere in the program.
An AI agent that hardcodes a zero quantity gets a compile error. An agent that parses a quantity from a request is forced to deal with the invalid case, because the refinement returns an Either instead of a bare number. A refined type puts the spec's value rule in the one place an AI agent can't skip.
How One Type per State Enforces State-Driven Spec Requirements
State-driven requirements describe what the system must or must not do while it sits in a given state. The typical failure in generated code is a valid operation called on an order in the wrong state, and giving each state its own type moves that failure from production to the compiler.
The payment function only accepts a Placed order, so no generated code path can capture payment on a backordered one. The agent can still write a clumsy workflow, but a workflow that breaks the state rule won't compile. Sealed traits like this one are among the Scala features that matter most once AI agents write a real share of your code.
How Exhaustive Error Types Enforce Failure-Case Spec Requirements
Unwanted-behavior requirements are where generated code most often goes wrong, because generated code tends to favor the happy path. Model every failure the spec names as a case in a closed type, turn on -Werror, and the compiler refuses any match that forgets a case.
If the agent drops the shortfall branch, or a later change adds a failure case nobody handled, the build stops. With exhaustive error types, the failure modes listed in a spec become a checklist the Scala compiler enforces on every run.
How Property Tests Enforce Event-Driven Spec Requirements
Event-driven requirements describe a trigger and a response, which makes them the best fit for property-based testing. A ScalaCheck property states the rule once and then tests it against 100 generated orders and inventories by default, and more if you configure it,, including the odd combinations that hand-written example tests tend to miss.
For property tests to work as guardrails, a person writes the property from the spec before the agent writes the implementation, and the agent's job is to make the property pass. When a property fails, ScalaCheck reports the input that broke it, shrunk to the simplest failing case, which makes a far more useful next prompt than a vague note that the tests failed.
How Module Boundaries and Lint Rules Enforce Spec Scope Limits
Scope statements tell the system what it must never do, and scope is the easiest kind of requirement for an agent to drift past. An agent asked to fix a domain bug will happily import an HTTP client if one sits on the classpath. sbt module boundaries and Scalafix DisableSyntax rules are two cheap ways to keep generated code inside the spec's scope.
The domain module can't import the HTTP layer because the dependency doesn't exist, and the lint step rejects nulls, mutable variables, and thrown exceptions anywhere the rule applies. WartRemover covers similar ground if your team already uses it, and either tool turns a "shall not" line in the spec into a failed CI run.
How to Turn a Spec Into Guardrails for an AI Coding Agent
Turning a spec into guardrails for an AI coding agent takes seven steps, and the order of the steps matters. Each step closes a gap the previous step leaves open.
- Write the scope first. Start with "shall not" statements about what the system won't do, then write functional requirements inside that boundary. An agent working from a spec with no stated scope fills the gap with its own assumptions.
- Tag every requirement with its cheapest check. Next to each spec line, note the mechanism that will reject a violation: refined type, state type, error type, property, module boundary, or lint rule. Spec lines with no possible automated check go on a separate human-review list. A constitution.md file full of architecture rules counts as prose here too, since the agent can read every rule and still break one.
- Write the types before the logic. An engineer encodes the domain types, states, and error cases first, reviews them against the spec, and commits them. The agent then fills in implementations against signatures it didn't design.
- Write the properties before the implementation. Each event-driven requirement becomes a ScalaCheck property. The agent's task turns into making the properties pass, which gives it a concrete target instead of a paragraph to interpret.
- Lock the gates. Put build settings, compiler flags, lint config, and property files under CODEOWNERS with branch protection set to require code-owner review, so any agent-authored change to them needs human approval. CI should run the same checks from a config the agent can't touch.
- Feed every failure back as the next prompt. Compiler errors, failing properties, and lint violations go straight back to the agent with the exact message and input. Specific failures produce specific fixes, and the loop runs without a reviewer stuck in the middle.
- Review the residue by hand. Spec requirements that couldn't become an automated check get a human reviewer on every change. Keep the human-review list short and visible, because that list is where the real risk in AI-generated code now lives.
Why an AI Agent Should Never Edit Its Own Guardrails
A coding agent told to make a failing check pass has two options, fixing the code or weakening the check. Agents can take the second route by deleting an assertion, adding a @nowarn annotation, relaxing a compiler flag, or rewriting a property until it tests nothing. None of those changes looks dramatic in a diff, which is exactly how they slip through review.
Keep a clean line between the files that define correctness and the files the agent writes. Types, properties, build settings, and lint rules belong to people, while implementations belong to the agent. Any agent-authored change that crosses that line should block the merge until a person signs off.
Tests the agent does write need their own check, since an agent can produce a suite that touches every line and catches almost nothing. Mutation testing with Stryker4s plants small bugs in the code and reports how many of them the tests miss. A suite that lets most of those planted bugs survive is decorative, and the surviving bugs make useful prompts for the agent's next round.
What a Spec Can't Turn Into an AI Agent Guardrail
Guardrails prove that code matches the spec. Guardrails can't prove that the spec matches the business, and some requirements resist encoding entirely.
- Judgment calls in customer-facing behavior. A requirement like "show a clear message when an order is backordered" can be checked for presence, but no compiler can judge whether the message is clear.
- Performance under real load. Latency and throughput rules only become enforceable with a benchmark gate in CI, and many teams don't build one until something breaks.
- Whether the spec is right. A wrong requirement encoded perfectly produces a wrong system that passes every check. Someone who knows the domain still has to review the spec before any of this starts.
Formal verification tools like Stainless can push the boundary further for small pieces of pure, high-stakes logic, such as interest or fee calculations. Outside those narrow cases the cost usually outweighs the gain, and types plus properties carry the load.
Spec guardrails carry a real upfront cost, because encoding types and properties before generation slows down the first days of a feature. The payback arrives when the agent starts producing volume, because every generated line gets checked against the spec without a person reading it first.
Why Scala Spec Guardrails Depend on the Engineers Who Write the Types
In this workflow the agent writes most of the implementation, and people write everything that decides whether the implementation is correct. Turning "while an order is backordered, the order service shall not capture payment" into a signature that makes the violation impossible is a design skill. Experienced Scala engineers make that translation almost by reflex, while engineers newer to typed functional programming often encode the same rule as a runtime if check that a generated code path can simply go around.
Type design skill is why AI coding agents make experienced Scala engineers more valuable instead of less. The bottleneck in Scala spec-driven development moves from typing code to designing the constraints the code must satisfy. Scala's type system gives AI agents more structure to work against than looser languages do, but the structure only helps when someone designs the types well.
Your spec is only as strong as the types behind it.
Scala Teams provides senior Scala engineers who turn requirements into the types, properties, and build rules that keep AI-generated code inside the spec. Talk to a Scala expert.
Frequently Asked Questions
Can a spec actually constrain what an AI coding agent generates?
A spec constrains an AI coding agent only when each requirement is converted into a machine check that rejects code breaking the rule. A spec written as prose shifts the odds of correct output but guarantees nothing. Types that won't compile, property tests that fail, and lint rules that block the merge turn a spec into enforceable guardrails.
What is the difference between a spec and a guardrail in spec-driven development?
A spec describes what the system should do, while a guardrail is an automated check that rejects code violating that description. Guardrails have three properties: a machine can evaluate them, they run on every generation, and the AI agent can't edit them. A spec line without a guardrail behind it is advice the agent may or may not follow.
Why is Scala a good fit for spec-driven development with AI agents?
Scala gives almost every kind of requirement a place to live in the code. Refined types enforce value rules, sealed traits and exhaustive matching enforce state and failure rules, ScalaCheck properties enforce event rules, and sbt modules with Scalafix enforce scope. Each of those mechanisms rejects bad AI-generated code automatically instead of relying on a reviewer to notice.
Should an AI coding agent write its own property tests?
An AI coding agent should not write or edit the property tests that grade its own implementation. A person should write properties from the spec before generation starts and protect those files with CODEOWNERS and required code-owner review. An agent that can edit its own tests can weaken them until a failing implementation passes.
Which level of spec-driven development needs guardrails?
Spec-anchored development, where the spec lives alongside the code and drives changes over time, depends on guardrails the most. Spec-first workflows set the spec aside after the first generation, so violations creep in unchecked. Spec-as-source workflows regenerate code from the spec on every change, which makes automated guardrails the only practical way to confirm each regeneration still follows the spec.
Which spec requirements can't be enforced automatically?
Requirements that depend on judgment, such as whether a customer message is clear, can't be fully enforced by a compiler or test. Performance rules need a dedicated benchmark gate before they become enforceable. No guardrail can confirm that the spec itself is correct, so a domain expert still has to review the requirements.