Behavior-Driven Development and Spec-Driven Development with OpenSpec

Behavior-Driven Development and Spec-Driven Development with OpenSpec

The advantage of using BDD techniques such as capturing our intent in Gherkin syntax is that we can generate Cucumber tests that then guide the coding agent to build the application.

There are two main goals in this walkthrough:

  1. Using a spec which is only in text or markdown to get the coding agent to build an application that is completely aligned with our intent — and call it done only when it has actually completed.
  2. Achieving spec as source, as described in Birgitta’s article on martinfowler.com about spec-driven development tools.

The overall guideline of the harness is to “make it easy to ‘do the right thing’ and work at the spec level, rather than the code level,” as mentioned in my InfoQ article on enterprise spec-driven development.

The Template

Code for this walkthrough is the behavior-driven-template. This is a subset of the intent-driven-template, scoped down to help us demo BDD only and focus on it.

The Sample Application: PollCast

We start with an empty repository initialized with OpenSpec and switched to a custom behavior-driven schema. PollCast is an application that allows users to cast their vote against polls created by other users. The product intent and the technology stack both go into the context section of OpenSpec’s config.yaml:

schema: behavior-driven

context: |
  PollCast is an intuitive web application allowing users to instantly create,
  share, and track custom polls. To prevent spam without the friction of
  passwords, the platform features a streamlined UI where participants simply
  cast votes using their email address as a unique identifier, guaranteeing
  highly accurate and duplicate-free poll results.

  Tech stack: Node.js, server-side rendered Express, JSON file store to begin with.

With that context in place, the first slice is what we type into the coding agent — allow users to vote for a poll that already exists:

/opsx:propose Allow users to vote for a poll that already exists.

Rules:
- A vote cannot be modified once it is cast.
- A duplicate vote cannot be cast for the same email ID —
  email ID is the unique identifier.

A Behavior-Driven OpenSpec Schema

The behavior-driven schema is close to the standard OpenSpec schema — it generates a proposal, spec, design, and tasks — with two deliberate differences:

  1. The spec template contains fenced regions of Gherkin. Instead of a plain markdown template, the spec has fenced sections where Given/When/Then scenarios are articulated in Gherkin syntax. OpenSpec already supports this syntax in prose, but fencing the Gherkin means we can lint it, ensure it is syntax-valid, and generate acceptance tests from it.
  2. The tasks put acceptance tests first. The generated task list ensures the acceptance test suite is created before the application itself is built.

When the change proposal for poll voting is created, the spec includes a fenced Gherkin feature — including the rule that there are no duplicate votes on the same email ID — and the agent reports that the Gherkin has been extracted and linted as clean syntax. Two things make this work:

From Plain Text to Deterministic Tests

This brings us to the first question: how do we take a text file and ensure it guides the coding agent to build an application aligned with the intent articulated in the spec?

First, commit the spec and start a fresh session — I always like to commit the spec and begin the apply step in a new clean session so the coding agent has fresh context to work with. Running opsx apply then works through the tasks, and because of how the tasks are structured, the first thing that appears is an acceptance test folder containing Cucumber.js step definitions.

A quick primer if you are unfamiliar with BDD: Gherkin is just the syntax — the Given/When/Then scenario and feature structure. It is a ubiquitous language for business and product teams to collaborate on; it puts a specification in a standard format, and because Given/When/Then is nice to create tests from, it is very helpful for us. Cucumber is the tool that generates test cases from that Gherkin by defining a step for each line. We are using Cucumber.js since this is a JavaScript application; in Python you would use Behave, in Java JBehave or similar — each language has its own frameworks for step definitions.

Put the spec and step definitions side by side and the mapping is direct: a line like “a poll exists with options cats and dogs” in the spec has its programmatic equivalent as a function in the step definitions. Running these as tests produces a report showing every scenario in the spec passing — and the application has been built to satisfy them.

Why not just use something like the Playwright CLI? I use the Playwright CLI quite a bit, but it introduces an amount of non-determinism. Step definitions, on the other hand, are repeatable — the coding agent only changes a few things for each new feature, and it doesn’t burn a lot of tokens because you are just re-running code that is already built.

The alignment works in both directions. If I edit only the markdown spec — say Alice votes for dogs instead of cats — and re-run the acceptance tests, they fail: the vote was recorded for cats but the spec now expects dogs. The coding agent now has a deterministic signal to go and correct the code. The acceptance tests are always in line with the OpenSpec plain-text file.

The piece putting this together is the cucumber.cjs configuration, which was authored using an acceptance test authoring skill. That skill is currently built for Cucumber.js and JavaScript-based UI applications — it knows how to create a page object model, for example — and you can adapt it to the nature of your application and technology stack.

Here is a short summarizing this flow:

Two Rules and a Hook

How do we make the coding agent finish building the application and only then call it done?

Rule 2 is not just a guideline sitting in claude.md. A hook called Zone Guard enforces it as a deterministic check: within a git commit or within a session — depending on the approach you take — it will never allow a single piece of change to include both the spec and the code.

Spec as Source Harness Guidelines

The second question comes from Birgitta’s article on martinfowler.com and her framing of spec-anchored versus spec as source — which I compared OpenSpec against in an earlier post, concluding OpenSpec has the potential to be spec-anchored because of its source-of-truth specification. As an experiment, can we take it all the way to spec as source?

For spec as source, the spec is the only main source file that humans edit over time — humans never touch the code. As an extension, no vibe coding is allowed either: I cannot just ask the agent to modify the code and leave the spec behind. That is where the trouble usually starts — the spec builds the first version of the application, but the moment code is modified by hand, spec and code drift apart.

First, archive the completed change. The specification carries the delta format — added, removed, modified markers — so that when it is synced and archived it moves into the source-of-truth specification. The acceptance tests still pass; only their path changes, from inside the change folder to the source-of-truth specification.

Now, in a new session, let’s go against the grain of the harness — the new-engineer-on-the-team scenario:

Help me change the src folder directly to allow users to modify votes
by submitting with the same email again. No need to create OpenSpec proposal.

This deliberately contradicts the first specification, which disallows duplicate votes. Because the rules are in place, the agent pushes back and says a spec has to be created. Forcing it through with “bypass and move forward” doesn’t work either: the agent cannot complete the feature, because the acceptance test asserting a user must not be able to vote more than once with the same email ID is now broken by the modified code. The agent itself reports that the requested feature directly contradicts the existing spec, and that the path forward is to modify the spec first. The resolution: revert the changes and start afresh with a proper OpenSpec change proposal for the capability.

This is exactly what I articulated in my InfoQ article on enterprise spec-driven development: doing the right thing — writing the spec first — must be easy, and going against the grain of the harness — writing code first and leaving the spec behind — should be hard. Both are taken care of here, and the only way forward is an OpenSpec change proposal.

The new proposal’s spec again has fenced Gherkin, this time carrying the modified marker for the changed feature. Running the acceptance tests at this point shows the original scenarios still passing and the new scenarios as undefined — the modification is not yet done. There is more nuance here, such as calculating the effective test suite so that contradicting tests (the original feature and its modification) are not run together — a topic for a separate video.

Here is a short summarizing spec as source:

BDD Does Not Replace TDD

Because we are doing behavior-driven development does not mean TDD is not required. BDD takes care of the outside-in — the spec is always from the point of view of the user, and it drives the application from the outside. Test-driven development helps you go inside-out. Both are necessary to put an application together well; TDD is simply not covered here to keep the focus of this walkthrough on BDD.

Does this mean we have completely achieved spec as source? Not necessarily — there are a few more things to complete — but the two questions we set out with are answered: a markdown spec, given a programmatic equivalent through Cucumber.js step definitions, drives the application through tests that tell the coding agent when it is actually done; and the harness rules — acceptance tests must always pass, specs and code are never modified together — keep both humans and agents from inadvertently modifying code first.

Video Walkthrough

References


Read the full article on intent-driven.dev →

Originally published on intent-driven.dev on July 17, 2026

Hari Krishnan
Hari Krishnan — AI & Platform Strategist, Founder & CEO of Polarizer Technologies. Field notes on Spec-Driven Development, context engineering, and AI-native software delivery. About · harikrishnan.io · LinkedIn · GitHub