Skip to content

Spec-quality checklist gate

The spec-quality gate evaluates a feature spec against a small set of deterministic content rules before the orchestrator dispatches an implementer agent. Specs that fail any required rule are routed through an auto-fix loop with a bounded number of attempts; when the loop exhausts its budget the gate refuses to advance and surfaces the checklist report to the operator.

Why the gate exists

Tasks generated from a thin spec produce thin implementations and re-work loops. Catching the gap at spec time avoids spending agent context on under-specified work. Because the gate runs locally and is purely text-driven it has no LLM cost and adds milliseconds to the planning path.

Default rules

Rule id Required What it checks
acceptance_criteria_present yes The spec has an ## Acceptance criteria heading.
out_of_scope_present yes The spec has an ## Out of scope heading.
tested_via_present yes The spec mentions how the change is tested.
no_todo_markers yes No TODO markers remain in the spec body.
no_placeholder_tokens yes No <PLACEHOLDER>, TBD, or XXX tokens remain.
ref_paths_exist yes Every backtick-quoted path with a / separator exists in the workspace.

All rules are pluggable; project plugins register additional rules via the bernstein.spec_quality_rules entry-point group. Each entry must resolve to a zero-arg callable that returns a bernstein.core.planning.spec_quality.Rule. Optional rules (required=False) are reported but never block the gate.

CLI surface

bernstein spec check <path>            # evaluate, exit 2 on failure
bernstein spec check <path> --no-strict
bernstein spec auto-fix <path>         # heuristic fix loop, dry-run by default
bernstein spec auto-fix <path> --write

--max-iter (default 3) controls how many auto-fix iterations are attempted before the gate refuses to advance. The local heuristic patcher adds missing section headings and rewrites stray TODO markers; semantic gaps (e.g. a missing referenced file) require operator intervention.

Pipeline integration

Orchestrator code that dispatches an implementer agent should wrap the call site with refuse_to_advance:

from bernstein.core.planning.spec_quality import (
    SpecQualityUnresolvedError,
    refuse_to_advance,
)

try:
    report = refuse_to_advance(
        spec_path,
        workspace_root=repo_root,
        autofix=heuristic_or_llm_fixer,
        max_iterations=3,
    )
except SpecQualityUnresolvedError as exc:
    # Surface ``exc.report`` to the operator; do not dispatch the
    # implementer.
    return

refuse_to_advance returns the final report when the gate passes and raises SpecQualityUnresolvedError when it does not. The exception carries the last ChecklistReport so callers can render the failed items without re-evaluating the spec.

Authoring new rules

A rule is a small dataclass containing an id, description, and a callable:

from bernstein.core.planning.spec_quality import Rule, RuleResult

def _check(spec_text, workspace_root):
    if "stakeholder" not in spec_text.lower():
        return RuleResult(
            rule_id="stakeholder_named",
            passed=False,
            message="Spec does not name a stakeholder.",
            hint="Add a 'Stakeholder' heading or sentence.",
        )
    return RuleResult(rule_id="stakeholder_named", passed=True)

def factory() -> Rule:
    return Rule(
        rule_id="stakeholder_named",
        description="Spec names a stakeholder.",
        check=_check,
    )

Register the factory in your plugin's pyproject.toml:

[project.entry-points."bernstein.spec_quality_rules"]
stakeholder_named = "my_pkg.spec_rules:factory"

Plugins discovered at startup are merged after the default rule set; rule ids must be unique across the registry. Rules that raise during evaluation are converted into a failing RuleResult so a broken plugin cannot crash the gate.