Skip to main content

gherkin-derive

Derive Gherkin scenarios directly from a codebase — standalone, with no prior legacy-modernization analysis. Discovers the public surface (OpenAPI, routes, existing tests, exported signatures, plus message-queue, cron, and websocket/GraphQL surfaces), recommends a BDD binding mode via the bdd-value-guide rubric, and merges scenarios into `.feature` files (preserving prior enrichment, never overwriting) plus (in bdd-runner mode) pending step-definition stubs. Use it on its own to capture intended behavior before changing tests, or as Phase 3 of `/test-improve`. Creates no tracker Stories.

Source facts

Repository
bdfinst/agentic-dev-team
Last source activity
August 4, 2026 at 21:30
Detected SKILL.md language
English
Stars
302
Forks
35

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
gherkin-derive
description
Derive Gherkin scenarios directly from a codebase — standalone, with no prior legacy-modernization analysis. Discovers the public surface (OpenAPI, routes, existing tests, exported signatures, plus message-queue, cron, and websocket/GraphQL surfaces), recommends a BDD binding mode via the bdd-value-guide rubric, and merges scenarios into `.feature` files (preserving prior enrichment, never overwriting) plus (in bdd-runner mode) pending step-definition stubs. Use it on its own to capture intended behavior before changing tests, or as Phase 3 of `/test-improve`. Creates no tracker Stories.
argument-hint
<repo-path> [--mode none|xunit-with-annotations|bdd-runner] [--repo-slug <slug>]
role
worker
user-invocable
true
allowed-tools
Read, Glob, Grep, Bash, Write
# Gherkin Derive Role: worker. A **standalone** Gherkin derivation skill. It derives scenarios for a repo's public surface directly from code — it does **not** require any prior `/cd-test-architecture` analysis, and it does **not** create tracker Stories (the calling orchestrator owns triage). `/gherkin-public` remains a public-boundary Gherkin authoring worker used standalone. Usable two ways: on its own to capture intended behavior before any test change, or as the Phase-3 sub-step of `/test-improve`. ## Parse Arguments - Positional: `<repo-path>` (default: cwd). - `--mode <none|xunit-with-annotations|bdd-runner>` — the binding mode. When omitted, present the BDD value rubric (Step 1) and let the operator choose. - `--repo-slug <slug>` — namespace for the surface inventory under `.claude/memory/<workflow>/<slug>/` (`/test-improve` passes `--workflow test-improve`). ## Step 1 — Choose the binding mode (BDD value rubric) If `--mode` was **not** supplied, present the 5-question rubric from `knowledge/references/bdd-value-guide.md` and recommend a mode from the score: - `≥ 3 yes` → **`bdd-runner`** - `1–2 yes` → **`xunit-with-annotations`** - `0 yes` → **`none`** Show the recommendation and let the operator override. The three modes: - **`none`** — emit no Gherkin. Exit immediately with a one-line recommendation to use plain xUnit (e.g. *"0/5 BDD signals — use plain xUnit; no `.feature` files written."*). **Write no files.** - **`xunit-with-annotations`** — derive scenarios and write `.feature` files, but do **NOT** wire a BDD runner or install any framework. The files are documentation that `/build` cites in test method names and leading comments. - **`bdd-runner`** — derive scenarios, write `.feature` files, wire the language-appropriate BDD framework (Step 4), and generate pending step definition stubs. ## Step 2 — Discover the public surface No pre-computed component map is required. Discover surfaces in this **priority order**, most authoritative first: 1. **OpenAPI / Swagger spec** (`openapi.yaml`, `openapi.json`, `swagger.json`) — the most authoritative description of the public surface. Each path+method is a surface. 2. **Route definitions** — Express/Fastify handlers, Spring `@Controller` / `@RestController`, ASP.NET `[ApiController]`, Go `http.HandleFunc` / Chi / Gin routes. Each registered route is a surface. 3. **Existing test names** — `describe` / `it` / `[Fact]` / `@Test` blocks. These yield **characterization** scenarios (current behavior, not intended behavior). Use them as the primary source only when OpenAPI and routes do not already cover the surface. **Never treat a test's assertion as ground truth on its own** — before accepting a test-derived scenario, cross-check it against any other available signal (docstrings, comments, adjacent OpenAPI/route info, obvious status-code conventions). Record what was cross-checked (or that nothing was found) per Step 3. 4. **Public function signatures + docstrings** — exported functions/classes with doc comments. The lowest-priority fallback for libraries with no HTTP surface. Stop climbing the list for the **same surface description** once a higher-priority source covers it — do not duplicate a route's success/failure scenarios from its tests. But do not let this rule discard information: even when a surface is already covered by OpenAPI or a route, still scan its existing tests for error/edge branches that are **not** present in the documented spec, and add those as *supplemental* characterization scenarios rather than dropping them. **Graph-assisted discovery.** Prefer CodeGraph/Repowise over raw `Grep` for locating routes, handlers, and exported signatures — see [`knowledge/codegraph-vs-graphify.md`](../../knowledge/codegraph-vs-graphify.md) for tool selection and the fallback contract. **Async / event / scheduled surfaces — a separate discovery pass, run regardless of the cascade above.** These have no OpenAPI equivalent and the 1–4 cascade will never find them, yet Step 3 already has templates (**Batch / Scheduled Job**, **API / Event Consumer**) waiting to describe them. Scan for: - **Message-queue consumers/producers** — Kafka `@KafkaListener` / `KafkaConsumer`, SQS handlers, RabbitMQ `@RabbitListener`, generic `consume(...)` / `on_message(...)` callback registrations. - **Scheduled/cron entry points** — Spring `@Scheduled`, `node-cron` / `cron.schedule(...)`, Quartz jobs, Kubernetes `CronJob` manifests. - **WebSocket / GraphQL handlers** — `@SubscribeMessage`, `io.on(...)` / `socket.on(...)`, GraphQL resolver definitions (`Query`/`Mutation`/ `Subscription` fields). Each hit is its own surface: route message-queue and event hits to the **API / Event Consumer** template, and cron/scheduled hits to the **Batch / Scheduled Job** template. **In-depth, codebase-wide analysis is the mandatory default — always on, never operator-supplied (issue #1450).** Every invocation of this step performs an in-depth analysis of the codebase, not a shallow scan limited to registered entry points. Beyond the cascade above and the async/event/ scheduled sweep, explicitly analyze **controllers, handlers, services, domain logic, workflows, validation rules, error handling, and business processes** to identify behavior-driven scenarios that an entry point's bare signature does not reveal — e.g. a thin route handler backed by a service that enforces three distinct business rules yields three scenarios, one per rule, not one generic "invalid input" scenario. This depth requirement is unconditional: it is never gated behind an operator-supplied instruction, a flag, or a specific prompt phrasing — a run that skips this analysis is a spec violation of this skill, not an acceptable shallow default. Record what was analyzed for each of these eight categories in the Step 5 surface inventory's `## Analysis Coverage` section (see Step 5), so a thin run is detectable rather than merely asserted. **Resolve the existing file before authoring (issue #1420).** Run `detect_bdd_convention.py` once per repo to get the project's `.feature` destination directory, then compose each surface's path yourself as `<dir>/<surface>.feature` — `detect_bdd_convention.py`'s own contract stays a single project-wide directory probe; this skill composes the per-surface path, it never asks the script to resolve one itself: ``` python3 "${CLAUDE_PLUGIN_ROOT}/scripts/detect_bdd_convention.py" ``` A `"dir": null` result (`"signal": "none"`) means the script found no usable convention — including, since issue #1462, a repo whose only existing `.feature` root sits under a non-conventionally-named directory. Never compose a path from `null`; fall back to `../plan/references/gherkin-persistence.md`'s no-signal handling (prompt the operator, or its non-interactive default) instead of guessing a destination. If a file already exists at that composed path, **read it** before authoring anything for that surface — Step 5 merges into it rather than overwriting. **Surface identifiers must be unique per method+path (or equivalent distinguishing element for non-HTTP surfaces) — never collapse two different surfaces onto the same `<surface>` stem (issue #1526).** For an API Provider surface, derive `<surface>` from both the HTTP method and the normalized path (e.g. `GET /users/{id}` → `get_users_id`, `DELETE /users/{id}` → `delete_users_id`) so two endpoints that differ only by method or path never compose the same file/feature-title pair. For a non-HTTP surface (message-queue consumer, cron job, CLI command, exported function), use the fully-qualified name (queue/topic name, job name, module.function) for the same reason. A surface-identifier collision is a defect regardless of how it happens — it silently merges two unrelated behaviors' scenarios into one Feature block, where the exact-title dedup in Step 5 will treat a second surface's same-titled scenario as an existing duplicate and drop it. ## Step 3 — Author scenarios Use the same templates as `/gherkin-public`: **API Provider**, **UI**, **Batch / Scheduled Job**, **CLI / Library**, **API / Event Consumer**. Every scenario covers at least one success and one failure path, and every step is observable at the boundary — no internal calls. **Ground every failure path in an observed condition.** Before filling a failure-scenario placeholder, locate a specific failure condition actually present in the code — a conditional, a thrown/raised exception, a documented or observed HTTP status code, a validation rule — and cite it in the scenario body. When CodeGraph/Repowise are available, use `codegraph_explore` (or Repowise `get_context`/`search_codebase`) to inspect a surface's actual branches and error-handling depth for this; fall back to reading the source directly when the tools are unavailable. Do not invent a generic `<invalid request>` / `<failure-mode-summary>` placeholder as a paraphrase of the surface's name or signature. When no such condition is discoverable for a surface, mark that scenario `# TODO: no observed failure path — hand-author` instead of fabricating one — an honest gap beats an invented scenario. **Titles must be specific enough to identify the surface without relying on the `Feature:` header for context (issue #1526).** A `Scenario:` title read in isolation — in a CI report, a BDD runner's scenario list, or a coverage dashboard — must be recognizable as belonging to its specific surface, not a generic category label that could describe any endpoint (e.g. `returns error for invalid input`, `handles success case`). Prefer wording that names the concrete condition or resource involved (e.g. `rejects the request when the id path parameter is non-numeric`, `returns the created order with a 201 and Location header`) over a bare category name. This applies to success-path titles as well as failure-path titles — the no-generic-placeholder rule above already covers failure-path *content*; this rule covers *all* scenario *titles*. A title that is merely a paraphrase of the surface name (the same words as the `Feature:` line, reworded) fails this rule the same way a placeholder failure condition does. **Label the provenance** in each `.feature` file header: - Scenarios derived from OpenAPI or docstrings are **specification** scenarios (intended behavior). - Scenarios derived from existing tests or code are **characterization** scenarios — the header MUST state `# Characterization: current behavior, not intended behavior` so a reader never mistakes a captured bug for a spec. They are hypotheses about intended behavior, not confirmed specs. ```gherkin # Source: <openapi path | route | test file | signature> # Provenance: specification | characterization # Characterization: current behavior, not intended behavior (characterization only) # Cross-check: <docstring/OpenAPI/route signal that corroborates this, or "none found — unverified against intended behavior"> (characterization only) Feature: <surface> Scenario: <success path> ... Scenario: <failure path — a real observed condition, or the hand-author TODO> ... ``` **Detect drift in retained scenarios (issue #1420).** For each existing scenario retained (not replaced) during Step 5's merge, extract the observed condition for that same path exactly the way this step already does when authoring a fresh scenario — a status code, exception, or validation rule. Then call `gherkin_feature_merge.py check-stale` to decide match/mismatch deterministically, never by eyeballing the comparison yourself: ``` python3 "${CLAUDE_PLUGIN_ROOT}/scripts/gherkin_feature_merge.py" check-stale \ --existing <dir>/<surface>.feature --feature-title "<surface>" \ --observed "<scenario title>=<observed value>" --json ``` On a reported mismatch, leave the retained scenario's text unmodified — do not rewrite it — and record it for the Step 6 report. ## Step 4 — Wire the BDD framework (bdd-runner mode only) Skip this step entirely in `none` and `xunit-with-annotations` modes. Read `knowledge/test-stack-profiles/bdd-frameworks.md` for the per-language install steps and directory layout, then generate **pending** step-definition stubs so the suite **compiles and fails intentionally** (red before green) — never empty stubs that pass silently. | Language | Framework | Pending stub | |---|---|---| | JS/TS | Cucumber.js | `return this.pending();` | | Java (Maven) | Cucumber-JVM + `cucumber-junit-platform-engine` | `throw new io.cucumber.java.PendingException();` | | Java (Gradle) | Same via Gradle config | `throw new io.cucumber.java.PendingException();` | | C# | Reqnroll (xUnit / NUnit / MSTest) | `throw new PendingStepException();` (Reqnroll's own auto-suggested stub; `ScenarioContext.StepIsPending()` is deprecated as of Reqnroll 3.3.4 — see `bdd-frameworks.md`) | | Go | Godog | `return godog.ErrPending` | **Merge, never overwrite (issue #1421).** Step-definition stubs are merged into the existing file the same way Step 5 merges `.feature` scenarios — never a raw `Write`, which would silently discard any step a human (or `/build`) has already implemented. For each surface's step-definition file, write the newly-derived stub text (using the pending-stub form from the table above) to a scratch candidates file, then invoke `gherkin_stub_merge.py merge`: ``` python3 "${CLAUDE_PLUGIN_ROOT}/scripts/gherkin_stub_merge.py" merge \ --existing <dir>/<surface>_steps.<ext> --candidates <scratch-file> \ --ext <.js|.ts|.mjs|.cjs|.java|.cs|.go> --json ``` This is exactly one write path whether or not a step-definition file already existed at that path. Any step already bound in the file — pending or already implemented — is left byte-for-byte untouched; only genuinely new step patterns are appended, as pending stubs, after the file's last existing binding. **Exit 2 means no write occurred** — read the `--json` payload's `error` field to know why, and report the specific cause per Step 6 rather than a generic "could not merge" (never retry with a raw `Write`, whichever cause it is): - `unbalanced-braces` — the existing file's braces/parens don't lexically balance, so no binding's body can be safely bounded. The file needs hand-repair before any merge can succeed. - `dangling-annotation` — a step marker (a call, annotation, or attribute) was found with no attached, boundable body. Same remediation — hand-repair the file first. - `unsafe-path` — the composed `--existing` path contained a `..` component and was rejected before any read or write. Fix how the surface name was derived into a path; do not retry with the same value. - `malformed-candidates` — the scratch candidates file (this skill's own intermediate output, not the operator's step-definition file) itself couldn't be bounded. Re-author the candidates text for that surface and retry. - `unreadable-candidates` — the scratch candidates file (this skill's own intermediate output) is missing or couldn't be read at all. Re-check how this skill wrote that scratch file for the surface before retrying — not the operator's step-definition file. - `unsupported-extension` — the `--ext` value doesn't name a language this script recognizes (`.js`/`.ts`/`.mjs`/`.cjs`/`.java`/`.cs`/`.go`). Fix how this skill derived `--ext` from the surface's language before retrying. **Batch shared context-file edits (issue #1775).** When multiple surfaces discovered in this run add to the same shared acceptance-test-context file (e.g. a shared step-definition registration/support file, hooks module, or world/context setup shared across surfaces), collect every surface's addition to that shared file first, then apply them in **one edit pass** for that file — never reopen and re-edit the same shared context file once per step definition or per surface. Reopening it N times for N step registrations produces N redundant read/write round-trips for content that could have been composed once. **"One edit pass" still means one merge, never one raw `Write`, when the shared file is a step-definition/registration file** — collecting every surface's addition first is a batching optimization over how many times you invoke `gherkin_stub_merge.py merge`, not a license to replace it with a single `Write`; combine all the batched additions into one `--candidates` scratch file and make exactly one `gherkin_stub_merge.py merge` call against the shared file, per the Merge, never overwrite rule above (issue #1421). ## Step 5 — Output - `features/<surface>.feature` files (all non-`none` modes) — **merged, not replaced (issue #1420).** For each surface, write the newly-authored scenario text to a scratch candidates file, then invoke `gherkin_feature_merge.py merge` — never a raw `Write` — to produce the file on disk: ``` python3 "${CLAUDE_PLUGIN_ROOT}/scripts/gherkin_feature_merge.py" merge \ --existing <dir>/<surface>.feature --candidates <scratch-file> \ --feature-title "<surface>" --json ``` This is exactly one write path whether or not a file already existed at that path — a surface with no prior file goes through the same `merge` subcommand, which synthesizes a fresh block, so there is never a second, divergent write path to keep in sync. Any prior enrichment already in the file (hand-authored or from `/feature-coverage-analyzer`) — including `Background:` sections, `@tag`s, and `Scenario Outline:`/`Examples:` tables — is preserved byte-for-byte; only genuinely new scenario titles are appended, after the block's last existing unit. **Exit 2 means no write occurred** — read the `--json` payload's `error` field to know why, and report the specific cause per Step 6 rather than a generic "could not merge" (never retry with a raw `Write`, whichever cause it is): - `feature-not-found` — the named `Feature:` title isn't in the existing file; most likely a human renamed it. Reconcile `--feature-title` with the file's actual header. - `malformed-feature-block` — the title was found but the block's structure can't be bounded (a dangling `@tag` line, or a `Scenario Outline:` missing its `Examples:` table). The existing file needs hand-repair before any merge can succeed.
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub