Skip to content

Validation rule registry

cospec validate runs cospec's own rule families over every change, then — only for spec-bearing changes with delta files — delegates to openspec validate and merges its issues in. Rule IDs are stable public API: script against them, grep for them in CI logs, ignore them by ID if you ever need to.

For the verification-ledger row grammar (- [<state>] N.M @<layer> [(<owner>)] <probe> -> <result>) that the verification/* rules below enforce, see Verification. For the delta-file format (## ADDED/MODIFIED/REMOVED/RENAMED Requirements, #### Scenario: blocks) that the deltas/* rules enforce, see OpenSpec's writing-specs.md.

Command

cospec validate [name] [--all|--changes|--specs] [--strict] [--json] [--fast]
FlagEffect
(no args)defaults to --all
--strictpromotes every WARNING to blocking — this is what hooks and CI use
--fastskips the archive-precondition checks (used internally by apply)
--jsonmachine-readable output (shape below)

Exit code is 1 if there are errors (or warnings under --strict), otherwise 0.

Which families run for a change Every change always runs meta,

proposal, blockers, and tasks. Whether verification, design, deltas, and archive run depends on what the change's type declares — see Types & artifacts for the full per-type matrix. openspec validate is never invoked for a change whose type has no specs artifact, since its hardcoded no-deltas rule would false-error on a change that was never supposed to have any. :::

Levels

  • E — ERROR, always blocking.
  • W — WARNING, blocking only under --strict (shown below as "W (E-strict)").
  • I — INFO, never blocking.

Every issue also carries a one-line hint.

meta/

Structural checks on the change directory itself — .openspec.yaml, naming, and which artifact files are allowed to exist.

IDLevelCheck
meta/openspec-yamlE.openspec.yaml is present, parseable, and has a non-empty schema:; created: must be YYYY-MM-DD when present
meta/schema-unknownEthe declared schema isn't one of the eleven cospec types and isn't resolvable any other way
meta/legacy-schemaIschema resolves but isn't a cospec type — the change runs in legacy mode
meta/name-kebabEthe change directory isn't kebab-case with no YYYY-MM-DD- prefix (that prefix collides with archive naming)
meta/forbidden-artifactEa file exists for an artifact the type doesn't declare — e.g. a specs/ dir under ci
meta/unexpected-fileWa file matches no declared artifact glob (excludes README.md, .openspec.yaml, .refine/)
meta/empty-changeI.openspec.yaml exists but the change has zero artifacts yet — reported as "in progress," not as an error
meta/surface-unmetW (E-strict)a checked ## Surfaces box's consequence is missing, for a type whose target isn't Forbidden — specifically, a type like revert/build/ci whose verification.md doesn't exist at all. A missing row on a file that does exist is owned by verification/* instead, so this never double-reports.
meta/schema-outdatedIthe change is on schemaVersion 1 (absent counts as 1) — some artifacts are grandfathered out until cospec migrate; never blocks
change/artifact-missingI / E-strictan artifact required by the type's apply gate doesn't exist yet (verification is excluded — verification/missing owns that case)

proposal/

Section and content checks on proposal.md.

IDLevelCheck
proposal/missingW (E-strict when other artifacts exist)proposal.md is absent
proposal/sectionsErequired headings per variant are present: full types need ## Why, ## What Changes, ## Impact (plus ## Capabilities for feat, and fix when specs/ exists); lite types need ## Why, ## What Changes, ## Impact; every surfaced type additionally needs ## Surfaces, checked even when the section is empty
proposal/why-substantiveWfull-variant types only: ## Why body must be at least 50 characters
proposal/benchmarksEperf only: ## Benchmarks present with at least one before/after row
proposal/revert-citationErevert only: ## Reverts cites a backticked archive/ slug and/or a 7–40-char hex sha
proposal/surfaces-vocabEevery ## Surfaces checkbox token is one of the closed set defined in Types & artifacts — anything else fails closed

See Types & artifacts for the full artifact matrix and the ## Surfaces token vocabulary.

blockers/

Checks against blocking-changes.md's two gated sections.

IDLevelCheck
blockers/sectionsEboth gated headings are present with exact text; a near-miss prints the corrected heading
blockers/entry-grammarEevery line in a gated section is legal grammar; the corrected line is printed
blockers/dangling-refEan entry's slug is neither an active change nor an archived one
blockers/stale-uncheckedW (E-strict)an unchecked entry whose target has been archived — auto-fixable by sync-blockers
blockers/premature-checkedWa checked entry whose target isn't archived yet (allowed, but surfaced)
blockers/none-conflictENone. appears alongside actual entries in the same section

tasks/

Checks on tasks.md.

IDLevelCheck
tasks/has-tasksEat least one parseable - [ ] / - [x] item, when tasks.md exists
tasks/checkbox-grammarEa checkbox-like line OpenSpec's tracker won't parse (-[ ], * [ ], - [X ]) — the fixed line is shown
tasks/group-numberingW## N. groups are non-sequential, or N.M prefixes are inconsistent

verification/

Runs only for types whose schema declares a verification artifact. See Verification for the row grammar, layer vocabulary, and owner tags these rules check against.

The structural rules (missing, structure, row-grammar, layer-unknown, owner-unknown, evidence-required, deferred-reason) are fail-closed ERRORs for every type that has this family at all. The per-type required-row rules are ERROR when verification is required at apply time for that type (feat/fix/perf/refactor); the surface-driven rules and build/ci's deploy-real-layer are soft, because a checked ## Surfaces box only ever soft-promotes a requirement.

IDLevelCheck
verification/missingI / E-strictverification.md is required at apply time and absent
verification/structureEat least one ## N. behavior group exists; every group has at least one row
verification/row-grammarEa checkbox-like line doesn't parse as - [<state>] N.M @<layer> [(<owner>)] <probe> -> <result>
verification/layer-unknownE@<layer> is outside the closed layer vocabulary and isn't extended via openspec/config.yaml's verification.layers
verification/owner-unknownE(<owner>) is present and is neither (agent) nor (human)
verification/evidence-requiredEa [x] row's -> <result> is empty
verification/deferred-reasonEa [~] row has no non-empty defer: <reason>
verification/critical-real-layerEfeat only: a [critical] group has no row on a layer other than @unit
verification/reproduces-bugEfix only: no @regression row anywhere in the ledger
verification/equivalenceEperf only: missing a @benchmark row, an @equivalence row, or both
verification/invariantErefactor only: no @equivalence row
verification/deploy-real-layerW (E-strict)build/ci only, deploy surface checked: no @runtime row
verification/interactive-requiredW (E-strict)interactive surface checked: no @manual or @e2e row
verification/eval-checkW (E-strict)agent-behavior surface checked: no @eval row
verification/integration-checkW (E-strict)integration surface checked: no @integration row

design/

Fires only when the matching ## Surfaces box is checked in proposal.md, except seam-ownership, which always fires for refactor. All soft (WARNING, ERROR under --strict).

IDLevelCheck
design/operational-surfaceW (E-strict)feat/fix/refactor, interactive or deploy checked: no ## Operational surface section
design/integration-contractW (E-strict)feat/fix/refactor, integration checked: no ## Integration contract section
design/seam-ownershipW (E-strict)refactor, always: no ## Seam ownership section

deltas/

Spec-bearing changes only. These run before delegating to openspec validate, so cospec's own diagnostics win the report. See OpenSpec's writing-specs.md for the delta format these rules enforce.

IDLevelCheck
deltas/scenario-depthEa ### Scenario: heading uses three hashtags — it must be #### Scenario:
deltas/header-presentEeach specs/*/spec.md has at least one ## ADDED|MODIFIED|REMOVED|RENAMED Requirements header
deltas/requirement-shapeEADDED/MODIFIED requirements have a SHALL/MUST statement plus at least one #### Scenario:
deltas/capability-kebabEcapability directories are kebab-case, with the delta at specs/<cap>/spec.md

archive/

The archive-precondition family: set-membership checks against living-spec requirement names, mirroring OpenSpec's own preconditions. Runs at validate time (skipped by --fast) so an archive failure surfaces before you get to cospec archive. See apply and archive for how these preconditions relate to the archive command's runtime verifier.

IDLevelCheck
archive/no-opsEa delta file parses to zero operations
archive/target-missingEa MODIFIED / RENAMED-FROM / REMOVED target is absent from the living spec
archive/new-spec-non-addedEa capability with no living spec has MODIFIED/RENAMED/REMOVED operations
archive/added-existsEan ADDED target already exists, or a RENAMED-TO collides with an existing/ADDED name
archive/target-invalidEthe target living spec is structurally invalid (missing ## Purpose/## Requirements, or still contains delta headers)
archive/scenario-preservationW (E-strict)advisory mirror of the hard archive-time gate of the same name: a MODIFIED requirement drops its #### Scenario: count with no Scenario removed: <reason> note and no matching REMOVED operation

cospec can be stricter than OpenSpec here A false PASS at validate

time — cospec says OK but cospec archive still aborts — is treated as a release-blocking bug in cospec itself, and the archive command's own runtime verifier still catches it, so an archive is never silently corrupted. Parity between this family and the real archive behavior is checked by contract tests against the pinned OpenSpec binary. :::

The two hard archive gates

cospec archive runs two additional checks as explicit command steps, before delegating to OpenSpec's own archive — not folded into the table above, because a real breach here must never report as a clean archive. Both exit with the same failure code the tasks gate uses, and neither has a --force override:

  • archive/verification-incomplete — runs after the tasks gate, independent of specs (so it fires for a specs-less fix too). Whenever verification is required for this change's type, every row must be [x] with non-empty evidence or [~] with a reason; any bare [ ] row refuses the archive.
  • archive/scenario-preservation — runs immediately before OpenSpec's archive executes, for spec-bearing changes only. Same check as the advisory rule above, but as a hard block — this catches the class of scenario thinning that openspec archive would otherwise merge silently.

See Apply and archive for the full archive exit code table.

specs/

Runs when validating living specs directly (cospec validate --specs), which always delegates to openspec validate --specs and layers cospec's own check on top.

IDLevelCheck
specs/purpose-tbdW## Purpose still contains the archive-generated TBD - created by archiving placeholder
(delegated)every issue from openspec validate --specs --strict, re-rendered in cospec's format

Output shape

Human-readable output groups by change, keeps rule IDs greppable, and always prints a hint:

cospec validate — 2 changes, 5 specs

✗ add-widget  (feat)
  ERROR   specs/widgets/spec.md:14  deltas/scenario-depth   scenario heading uses 3 hashtags; must be `#### Scenario:`
  ERROR   blocking-changes.md:7     blockers/dangling-ref   `add-auth` is not an active or archived change
          hint: `cospec list` shows active changes; fix the slug or remove the entry
  WARNING proposal.md               proposal/why-substantive  ## Why is shorter than 50 characters
✓ fix-null-crash  (fix)
✓ specs: 5/5 valid

2 errors, 1 warning — validation failed

--json emits:

json
{
  "version": "...",
  "items": [
    {
      "id": "add-widget",
      "kind": "change",
      "type": "feat",
      "valid": false,
      "issues": [
        {
          "level": "ERROR",
          "rule": "deltas/scenario-depth",
          "path": "specs/widgets/spec.md",
          "line": 14,
          "message": "scenario heading uses 3 hashtags; must be `#### Scenario:`",
          "hint": null,
          "fixable": false
        }
      ]
    }
  ],
  "summary": { "errors": 2, "warnings": 1, "byRule": { "...": 1 } }
}

Each issue carries level, rule, path, an optional line, message, an optional hint, and fixable.