From exo
Write and automate BDD test specifications. Use "bdd-author write" to generate feature files from issue content, "bdd-author automate" to generate step definitions, or "bdd-author guard" to scan step definitions for user-perspective anti-patterns.
How this skill is triggered — by the user, by Claude, or both
Slash command
/exo:bdd-authorThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Generate BDD feature specifications and automate them with step definitions.
Generate BDD feature specifications and automate them with step definitions.
write, path to feature file for automate, path(s) to step definition file(s) for guardBefore generating any output, detect the project's BDD framework:
Check for existing BDD files:
*.feature files anywhere in the projectbehave.ini, cucumber.js, .cucumber, jest-cucumber configpytest-bdd in pyproject.toml, setup.cfg, or requirements*.txtInfer from project language if no BDD framework found:
pytest-bddcucumber-jscucumbergodogcucumber-jvmDetermine test directory:
tests/bdd/ (Python), features/ (Ruby/JS), or equivalent conventionFeature files carry traceability metadata via Gherkin @ tags. These tags link BDD tests back to requirements, issues, and acceptance criteria.
Tag conventions:
@REQ-XXX — links to the originating requirement ID (from req-decompose)@issue-N — links to the GitHub issue number@AC-N — links a scenario to a specific acceptance criterion in the issue bodyThe caller (typically the dev-loop agent) provides the REQ-ID and issue number. If not provided, omit those tags — only add tags for identifiers you actually have.
Generate a BDD feature file from issue content. Does NOT write automation code.
Usage: /bdd-author write
Provide the issue title and body as context (typically passed by the dev-loop agent). The caller should also provide the issue number and REQ-ID if available.
Every When step must be an action a real actor takes against the system's real entry point: HTTP request, CLI invocation, UI interaction, message publish, scheduled trigger, file drop. Every Then step must be an outcome that actor (or another observable role) can see, receive, or measure.
GOOD:
When the user submits the reset form with "[email protected]"
Then the user receives an email at "[email protected]" within 60 seconds
And the email body contains a single-use reset link
BAD (rewrite before authoring):
When the AuthService.reset_password method is called # internal call, not a user action
Then the response JSON has a "token" key # schema check, not observable behavior
And the mock email_sender was called with the user # mock-on-mock, not a real outcome
Each @AC-N-tagged scenario must trace to an AC that already passes the user-perspective rule (see req-decompose and nano-spec). If the AC reads as "implement endpoint X" or "add column Y", stop and report: the AC needs to be rewritten upstream before authoring a scenario for it. Do not paper over an implementation-shaped AC by inventing a user wrapper.
Steps:
<req-id>-<slug>.feature if a REQ-ID is available (e.g., req-001-user-authentication.feature), otherwise <issue-number>-<slug>.feature (e.g., 42-user-authentication.feature)..feature file in Gherkin syntax with traceability tags:@REQ-XXX @issue-N
Feature: <derived from issue title>
<one-line description derived from issue body>
Background:
Given <common preconditions if any>
@AC-1
Scenario: <maps to acceptance criterion 1>
Given <initial state>
When <action>
Then <expected outcome>
@AC-2
Scenario: <maps to acceptance criterion 2>
Given <initial state>
When <problematic action>
Then <expected error handling>
@AC-N matching the criterion's position in the issue bodyBDD Framework: <detected or recommended framework>
Feature file: <path to generated .feature file>
Traceability: REQ-XXX → #N (or "no REQ-ID provided")
Scenarios: <count> (<brief list with @AC-N mappings>)
Note: <any setup instructions if framework not yet installed>
Generate step definitions / test glue code for an existing feature file.
Usage: /bdd-author automate
Provide the path to the feature file as context (typically passed by the dev-loop agent).
Step bodies MUST:
bdd-author guard action verifies this comment is present.These rules are checked by the guard action. Violations block the dev-loop pipeline.
Steps:
conftest.py, test_*.py, *_steps.pystep_definitions/, steps/# entry: <real entry point> | observe: <observable output> (or // entry: ... | observe: ... for JS/TS). Example:Python (pytest-bdd) example:
from pytest_bdd import given, when, then, scenarios
from starlette.testclient import TestClient
from app.main import app
scenarios("<feature_file>.feature")
@given("a registered user with email \"[email protected]\"")
def given_registered_user(db):
# entry: direct DB seed (test fixture) | observe: row in users table
db.execute("INSERT INTO users(email) VALUES ('[email protected]')")
@when("the user submits the reset form with \"[email protected]\"")
def when_submit_reset(context):
# entry: POST /reset-password via TestClient | observe: HTTP response
client = TestClient(app)
context.response = client.post("/reset-password", json={"email": "[email protected]"})
@then("the user receives a reset email at \"[email protected]\" within 60 seconds")
def then_reset_email_received(smtp_sink):
# entry: read from real test SMTP sink | observe: email visible to recipient
msg = smtp_sink.wait_for(to="[email protected]", timeout=60)
assert "reset" in msg.subject.lower()
assert "/reset?token=" in msg.body
JavaScript (cucumber-js) example:
const { Given, When, Then } = require("@cucumber/cucumber");
const request = require("supertest");
const app = require("../../src/app");
When("the user submits the reset form with {string}", async function (email) {
// entry: POST /reset-password via supertest | observe: HTTP response
this.response = await request(app).post("/reset-password").send({ email });
});
Then("the user receives a reset email at {string} within 60 seconds", async function (email) {
// entry: read from test SMTP sink | observe: email visible to recipient
const msg = await this.smtp.waitFor({ to: email, timeoutMs: 60000 });
expect(msg.body).toMatch(/\/reset\?token=/);
});
conftest.pyfeatures/step_definitions/<feature_name>_steps.{js,rb}entry: ... | observe: ... comment. Implement the body using a real entry point per the Step Definition Rules. If the implementation isn't ready, write the body to drive the real entry point and assert on the observable output anyway — the test can fail until the implementation lands. Do NOT replace the body with a TODO and a pass.requirements.txt or pyproject.tomlnpm install --save-dev @cucumber/cucumber@ traceability tags from the feature file. Step definitions must not strip or ignore @REQ-XXX, @issue-N, or @AC-N tags.Step definitions: <path to generated file>
Steps automated: <count> (<count> new, <count> existing)
Traceability tags preserved: @REQ-XXX, @issue-N, @AC-1, ...
Dependencies added: <list or "none needed">
Scan step definition files for user-perspective anti-patterns. Hard fails the dev-loop pipeline on findings.
Usage: /bdd-author guard
Provide the path(s) to the step definition file(s) (typically passed by the dev-loop agent after automate runs). Optionally pass the feature file path for context.
Anti-patterns checked
For each step body in each step-definition file, flag:
Mock-on-mock. The body's only assertions are mock-call assertions. Detection regexes (per language):
\.assert_called(_with|_once|_once_with)?\(, \.called\b, assert_not_called, \.call_count\b\.toHaveBeenCalled(With|Times)?\(, \.toHaveBeenLastCalledWith\(, expect\(.*\.mock\)have_received\(, should have_receivedverify\(.*\)\., Mockito\.verify
Trigger if these matches exist AND the step body contains no other assertion (assert, expect(...)\.(toBe|toEqual|toMatchObject|toContain), should ==, assertThat).Schema-only. The only assertion is a schema validation. Detection regexes:
validate_schema\(, jsonschema\.validate\(, pydantic model parse with no field assertion afterwards\.toMatchSchema\(, ajv\.validate\(, joi\.\w+\.validate\(, zod.*\.parse\( with no field assertionschema in the function name and no value-level assertion in the same body
Trigger if the body validates structure but never asserts a specific value, status code, or piece of state.No real entry point. The step body calls into project code directly without going through an entry point. Heuristic:
src/, app/, lib/, or the project's package).requests, httpx, urllib, TestClient, supertest, fetch, axios, subprocess, Popen, page, browser, playwright, a broker client (kafka, redis, pika, nats), or filesystem I/O for a real input file.
Trigger if the body bypasses the entry point boundary.Missing entry/observe comment. No comment matching (?i)(?:#|//)\s*entry:.+\|\s*observe:.+ within the first 3 lines of the step body. Trigger always.
Steps:
@given/@when/@then decorator (Python) or the callback inside Given/When/Then(...) (JS/Ruby).(file, line, anti_pattern, snippet).Important: The guard reports findings; it does NOT auto-fix. The dev-loop agent runs the auto-fix loop by re-invoking bdd-author automate with the guard report as input.
BDD Guard: PASS | FAIL
Files scanned: <count>
Step bodies scanned: <count>
Findings: <count>
Findings:
- <file>:<line> [<anti-pattern>] <step text>
snippet: <first ~80 chars of the offending body>
why: <one-line explanation>
If BDD Guard: PASS, no anti-patterns were found and the dev-loop pipeline may proceed. If FAIL, the dev-loop pipeline must hard-fail and trigger the auto-fix loop.
npx claudepluginhub ubiquitousthey/exo --plugin exoProvides behavioral guidelines to reduce common LLM coding mistakes, focusing on simplicity, surgical changes, assumption surfacing, and verifiable success criteria.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.