Lint specs in CI with a spec linter
A spec linter is a program that checks a specification for structural mistakes and exits with a failure code when it finds them. Continuous integration (CI) is an automatic check on every proposed change. Continuous integration and continuous delivery (CI/CD) is the pipeline that runs that check and keeps a passing change releasable. Spec-driven development (SDD) is the practice of writing the specification before the code and treating the specification as the source of truth. A product requirements document (PRD) is the prose that states the problem and the scope. In SpecScore, a requirement (REQ) is a named rule inside a feature, and an acceptance criterion (AC) is an optional abstract condition that points at one or more of those rules. The check is a command-line interface (CLI) command, specscore spec lint, from the CLI in specscore/specscore-cli (Apache-2.0). Easy Approach to Requirements Syntax (EARS) is a sentence pattern for a requirement. This linter checks headings, required sections, and references. It does not grade the English.
Last updated: 4 November 2026
What linting a specification means
Linting a spec is the same kind of check as linting code. A deterministic program reads the files and reports rule violations. It does not decide whether the product is a good idea, and it does not rewrite the requirement into better prose. The validators page separates that structural check from a manual AI review. specscore spec lint is the structural check.
The command’s description, in internal/cli/spec.go at CLI tag v0.55.1 and at the v0.39.1 tag this repository’s workflow installs, says it validates the spec tree for structural convention violations. A clean run prints 0 violations found and exits 0. A run that finds violations prints one line per finding and exits 1. That exit code is what fails a CI job. Invalid arguments exit 2. A missing specscore.yaml exits 3, and the message tells you to run specscore init.
What the spec linter checks
The rules are the CLI’s, not a separate cloud service. The generated catalog docs/lint-rules.md at tag v0.55.1 lists 183 rules. This website’s workflow installs v0.39.1, an older release, so a rule that exists only in a later tag is not what that job runs. The rules named in this section are in both tags. The default --severity is error. A warning is printed only when you pass --severity warning or info, and the default command does not fail on it. The catalog marks dogfood-version-bump as a warning, so the default command does not fail when the workflow pin is behind the binary.
Required files and sections
Rule readme-exists (readme_exists.go) requires a README.md in every spec directory. Rule oq-section (oq_section.go) requires a feature README to contain an ## Open Questions section. A legacy ## Outstanding Questions heading is a separate finding, and --fix rewrites that heading in place. The message in the example below, “Open Questions section not found”, is the finding from a feature README that had the section removed.
Index rows and child directories
Rule index-entries (index_entries.go) checks a feature index against the child directories on disk. A child that is not listed is a violation, and a row whose link points at a child that is not there is a violation. --fix can delete the phantom row and insert the missing one. This is directory and index integrity. It is not a crawl of every URL in the prose.
Acceptance-criteria heading shape
Rule ac-heading-format (ac_heading.go) applies to a feature README, a file whose title is # Feature:. A line that looks like an acceptance-criterion heading must be exactly ### AC: <id> with one space after the colon and a kebab-case id. A heading of the form ### AC: <id> (verifies ...) is also accepted. --fix repairs spacing only. An id with uppercase letters or underscores is reported and left for a person. The rule does not require the **Requirements:** line, does not score words such as “graceful” or “fast”, and does not warn when the prose contains Given, When, or Then. How to write the criterion itself is in how to write acceptance criteria.
Plan references to those IDs
On a plan that names a source feature, plan_rules.go reads ### AC: headings in that feature’s README. Rule P-002 reports a task **Verifies:** value that does not resolve to <feature-slug>#ac:<ac-slug> for one of those headings, an empty **Verifies:** line, a missing or unrecognized source line, or a source feature that does not resolve to a feature README. Rule P-001 reports a heading that no task covers and that is not listed under ## Deferred AC Coverage. Idea-sourced plans (**Source:** idea:<slug>) and plans with **Source:** none are exempt, and so is a plan in a terminal disposition status. The one-line catalog text for P-002 says “valid dependency references”; the checker in that file is the AC-reference check just described.
What a passing lint is not
The same command does not decide whether a requirement solves the stated problem, whether a name makes sense, or whether two features contradict each other in meaning. The validators page lists those as outside the CLI linter. It also does not check scenario step keywords. specscore rehearse runs scenario files. The workflow on this repository runs specscore spec lint and does not call rehearse. The catalog names further families — ideas, decisions, entities, properties, lessons, issues, and the rest of the plan rules — and each rule applies to the files it owns. The catalog is the list. This page does not restate all 183 descriptions.
Run specscore spec lint in CI
The command looks upward from the working directory for specscore.yaml. Without that file it exits 3 and names specscore init. A CI job therefore needs the config file in the checkout. scripts/install.sh reads SPECSCORE_VERSION and defaults to latest when the variable is unset. Pin a tag if you want the same rules on every run.
This repository’s check is dogfood.yml. It installs the CLI with SPECSCORE_VERSION set to v0.39.1 and then runs specscore spec lint with no extra flags, on pull requests and on pushes to main. GitHub Actions fails the step when that process exits non-zero. The excerpt below keeps those install and lint steps. The file also sets permissions: contents: read, a concurrency group, and a comment on why the pin is not latest.
name: Dogfood lint
on:
push:
branches: [main]
pull_request:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
- name: Install specscore CLI (pinned)
env:
SPECSCORE_VERSION: v0.39.1
run: |
curl -fsSL https://specscore.md/install/get-cli | \
SPECSCORE_INSTALL_DIR="$HOME/.local/bin" sh
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
specscore --version
- name: Lint spec tree
run: specscore spec lint
features/apply-discount/README.md:0 [error] oq-section: Open Questions section not found
features/apply-discount/README.md:0 [error] index-entries: Child directory not listed in index: refunds
features/apply-discount/refunds:0 [error] readme-exists: README.md not found
3 violations found (3 errors)
0 violations found
Each finding is file:line [severity] rule: message. The summary is N violations found plus a parenthetical count, such as (3 errors). The exit code is the process status, not a line in the report. The three lines above were captured from specscore 0.55.0 on a feature whose Open Questions section had been removed and whose index omitted an empty child directory. They are the same report the CLI page shows. --format accepts text (the default), json, or yaml. --rules and --ignore select rules and cannot be combined. --fix rewrites the findings that have a fixer, including the Outstanding Questions heading. The dogfood job does not pass --fix.
Tools that do not lint this format in CI
A requirements quality check, in this CLI, is the structural check above. It is not a score of the prose. Other spec-driven tools publish different checks. The paragraphs below use only what those projects’ own pages say. The tools roundup records the same pages as read on 30 October 2026, except the Spec Kit documentation home, which prints 28 September 2026, and the Kiro feature-spec page, which prints 4 August 2026.
GitHub Spec Kit is an agent workflow that writes Markdown artifacts. The agentic SDD reference describes /speckit.analyze as a report of inconsistencies across the spec, the plan, and the tasks, without editing the files. You run it in the agent. The documentation home also names community extensions, including one called CI Guard, that add compliance gates. That is not a command that lints a SpecScore feature README.
OpenSpec does publish a structural check. openspec validate, documented in the CLI reference, checks OpenSpec’s change and spec structure, exits non-zero when validation fails, and the reference includes a shell snippet a CI job can run. That check is OpenSpec’s shape, not ### AC: headings or a SpecScore index.
Kiro’s feature-spec page describes Analyze Requirements as a check inside Kiro, before design, looking for inconsistencies, ambiguity, conflicting constraints, and gaps. It is not the command in dogfood.yml. The sentence patterns Kiro writes are in EARS requirements syntax.
The BMAD Method README describes the method’s artifacts and agent workflows. It does not document a command that lints a fixed spec schema in CI. The Task Master README describes parsing a PRD into tasks with task-master parse-prd. It does not describe a linter for that PRD. A PRD can still be the input to a SpecScore feature, and specscore spec lint is what then checks the feature file.
FAQ
What is a spec linter?
A spec linter reads specification files and reports structural violations. specscore spec lint is that command for a SpecScore tree. It exits 0 when it finds nothing at the chosen severity, and 1 when it finds violations.
Does the spec linter judge requirements quality in the wording?
No. ac-heading-format checks the ### AC: heading. It does not score words such as “graceful” or “fast”, and it does not warn when a criterion contains Given, When, or Then. Wording is a writing task. The validators page separates that deterministic lint from the manual AI review.
How do I lint specs in CI?
Install the CLI with SPECSCORE_VERSION set to a release tag, then run specscore spec lint. The checkout needs a specscore.yaml. dogfood.yml is a working GitHub Actions example. It pins v0.39.1.
What does a failing run print?
One line per finding, file:line [severity] rule: message, then a summary such as 3 violations found (3 errors). The process exits 1. A clean tree prints 0 violations found and exits 0.
Do other spec-driven tools validate specifications in CI/CD?
OpenSpec’s openspec validate can fail a CI job, and it checks OpenSpec’s own change structure. Spec Kit’s /speckit.analyze runs in the agent session. Kiro’s Analyze Requirements runs inside Kiro. The BMAD Method and Task Master READMEs do not document a format linter for CI. SpecScore’s dogfood workflow runs specscore spec lint on the SpecScore tree.
Sources
CLI behavior checked 4 November 2026 against specscore-cli tags v0.55.1 and v0.39.1. Competitor pages are the ones linked below; the tools roundup records them as read on 30 October 2026, except where the page prints its own date.
- specscore/specscore-cli
- specscore-cli — Apache-2.0 licence
- specscore/specscore
- specscore-cli v0.55.1 — lint rule catalog (183 rules)
- specscore/specscore — dogfood.yml (CLI v0.39.1)
- specscore-cli — install.sh (SPECSCORE_VERSION)
- specscore-cli v0.55.1 — readme-exists
- specscore-cli v0.55.1 — oq-section
- specscore-cli v0.55.1 — index-entries
- specscore-cli v0.55.1 — ac-heading-format
- specscore-cli v0.55.1 — plan rules P-001 and P-002
- What SpecScore validates
- How to write acceptance criteria AI agents can verify
- EARS requirements syntax explained, with examples
- GitHub Spec Kit documentation (page date 28 September 2026)
- GitHub Spec Kit — agentic SDD reference (/speckit.analyze)
- OpenSpec CLI reference — openspec validate
- Fission-AI/OpenSpec
- Kiro — feature specs (updated 4 August 2026)
- bmad-code-org/BMAD-METHOD
- eyaltoledano/claude-task-master
- Spec-driven development tools
Related
- Spec-driven development with Claude Code and Cursor
- PRD template in Markdown for AI coding
- How to write acceptance criteria AI agents can verify
- EARS requirements syntax explained, with examples
- What SpecScore validates
- CLI
- Install the CLI
- Spec-driven development
- Spec-driven development tools
- Feature specification
- Acceptance criteria specification