Skip to content

Spec Structure Reference

A spec in this toolkit is three artifacts: the YAML frontmatter of specs/<N>-<slug>/spec.md, the per-spec specs/<N>-<slug>/state.yaml, and one entry under specs_status in the registry .design-state.yaml. Each has a JSON Schema (draft 2020-12). This page documents every field those schemas allow, and the parts a schema cannot express: the folder layout and the spec.md body sections.

The schemas are strict: a field that is not documented here is a violation (additionalProperties: false). Existing specs that deviate are recorded in a shrink-only baseline; new specs must match (SPEC-212).

Create a spec with /vt-d-spec-from-requirements or /vt-d-specs-from-prd rather than by hand: they allocate the id and write all three artifacts. This page describes what they write.

Artifact Schema
spec.md frontmatter spec-frontmatter.schema.json
state.yaml spec-state.schema.json
.design-state.yaml design-state.schema.json

Folder layout

specs/<N>-<slug>/
├── spec.md            the specification: YAML frontmatter + body sections (required)
├── state.yaml         lifecycle status and gate records (required)
├── decisions.md       shaping decisions from /vt-d-shape
├── research.md        research notes from /vt-d-2-plan
├── data-model.md      entities and fields, when the spec has any
├── plan.md            implementation plan from /vt-d-2-plan
├── tasks.md           task breakdown from /vt-d-2-plan
├── review-triage.md   disposition of review findings
├── contracts/         interface contracts (API, CLI, file formats)
└── checklists/        spec-specific checklists

<N> is the spec number without leading zeros, <slug> a kebab-case title: SPEC-052 lives in specs/52-<slug>/, SPEC-212 in specs/212-json-schema-for-the-spec-structure/. Spec numbers come from the allocator (plugins/vt-product-dev/skills/spec-from-requirements/scripts/allocate_spec_id.py), never from counting directories.

The smallest valid spec

specs/999-sample/spec.md starts with this frontmatter (the --- must be the file's first line):

---
id: SPEC-999
title: Sample
priority: P2
created: 2026-09-25
depends_on: []
---

specs/999-sample/state.yaml:

status: specified

.design-state.yaml (a new spec adds only its entry under the existing specs_status:):

specs_status:
  SPEC-999:
    file: 999-sample/spec.md
    status: specified
    specs_dir: specs/999-sample/

The three blocks, copied as printed into those files, pass plugins/vt-base/bin/spec-schema-check --all with exit 0.

spec.md frontmatter

Required: id, title, priority, created, depends_on.

Field Required Type / values Example Meaning
id yes SPEC- + at least 3 digits SPEC-212 The spec's identifier; equals its registry key.
title yes non-empty string JSON Schema for the spec structure Human-readable title.
priority yes P0 · P1 · P2 · P3 P1 Delivery priority, P0 highest.
created yes date YYYY-MM-DD 2026-09-24 Creation date (quoted or unquoted).
depends_on yes list of spec ids, unique, may be empty [SPEC-181] Blocking dependencies.
status no, deprecated spec lifecycle value specified Historical copy of the status. The authoritative status is in state.yaml and the registry; SPEC-181 removes it from the templates.
ui_scope no frontend · backend · mixed (default backend) backend Whether the spec touches UI; frontend/mixed need a ## Visual Reference section.
source no a path, or a non-empty list of paths intake/pending/from-rca/x.md Input document(s) the spec came from.
deliverable_types no non-empty list of deliverable types (default [code]) [code, test, document] What the spec delivers; decides which reviewers run.
parent_prd no string docs/prd/prd-v1.md PRD a spec was generated from (/vt-d-specs-from-prd).
tags no list of strings [schema, registry] Free keywords; nothing interprets them.
soft_depends_on no list of spec ids [SPEC-200] Non-blocking dependencies.

Not allowed (baselined where they exist): the legacy keys spec_id and dependencies, the source_* variants, and every other key.

state.yaml

Required: status. Every object below is strict too.

Field Required Type / values Example Meaning
status yes spec lifecycle value in_progress Authoritative lifecycle status; must equal the registry's (guard 54, scripts/tests/setup/54-spec-status-is-declared-and-agrees.sh).
activated_at no date or date-time, or null "2026-09-24T17:39:35" When /vt-d-activate started the spec.
completed_at no date or date-time, or null null When the spec was completed.
docs_page no string docs/user-manual/docs/features/x.md Documentation page this spec added or changed.
rejection_reason no string superseded by SPEC-200 Why the spec was rejected.
assignee no string rolf Who works on the spec.
steps no phase number → pending · in_progress · completed {1: completed, 2: in_progress} Build progress per tasks.md phase.
spec_check_gate no gate object see below Birth-gate record (/vt-d-spec-check).
shape_gate no gate object see below Shaping record (/vt-d-shape).
plan_gate no gate object see below Plan validation record.
build_gate no gate object see below Build record.
test_gate no gate object see below Test gate record.
review_gate no gate object see below Review record.
finalize_gate no gate object see below Finalization record.
gate_bypass no object see below Deliberately skipped gates (/vt-d-complete).
gate_history no list of history items see below Earlier passes of any gate, oldest first; the gate object holds the latest.

Gate objects

Every gate object may carry status, notes (free-text reasoning — status stays a vocabulary value), validator (agent · inline, SPEC-189) and validators_run (list of agent names). status is required except in shape_gate, where a skipped shaping writes only skipped: true.

Field Required Type / values Example Meaning
notes no string re-validated after rev. 3 Explanation of the verdict.
validator no agent · inline agent Who produced the verdict; absent reads as unknown.
validators_run no list of strings [plan-checker, strategic-mentor] Validator agents that actually ran.
spec_check_gate.status yes RUN RUN The birth gate ran (it never blocks).
spec_check_gate.date no date 2026-09-24 When it ran.
spec_check_gate.spec_sha256 no 64 hex characters 9422a2de… (shortened here) SHA-256 of spec.md at that time.
shape_gate.date no date 2026-09-24 When shaping completed.
shape_gate.decisions no integer ≥ 0 7 Decisions locked in decisions.md.
shape_gate.skipped no boolean true Shaping was skipped.
shape_gate.reason no string small spec Why it was skipped.
plan_gate.checked_at no date 2026-09-24 When the plan was validated.
plan_gate.principles_checked no list of strings [I, II, III, IV] Constitution principles checked.
plan_gate.violations no list [] Constitution violations found.
plan_gate.artifacts no list of strings [plan.md, tasks.md] Planning artifacts produced.
build_gate.date no date 2026-09-24 When the build gate was set.
build_gate.commits no integer ≥ 0 12 Commits on the branch at that time.
build_gate.verification no object see next row Test verification result.
build_gate.verification.command · .iterations · .status · .skipped · .reason no string · integer 1–5 · pass · boolean · string npm test, 2, pass The convergence loop's outcome.
review_gate.date · .branch · .head no date · string · commit id (7–40 lowercase hex, write it quoted) 2026-09-24, feature/x, "6a0fedb2c" What was reviewed, when. An unquoted all-digit SHA loads as a number and fails.
review_gate.critical · .high · .medium · .low no integer ≥ 0 0 Findings left open, per severity (D15).
review_gate.completeness no COMPLETE, INCOMPLETE, COMPLETE_FOR_REDUCED_ROSTER COMPLETE SPEC-192: whether the pass finished. Any uncovered reviewer forces INCOMPLETE, and an INCOMPLETE pass is never PASS — it is FAIL, or explicitly ungraded. COMPLETE_FOR_REDUCED_ROSTER is a legacy value recorded once by SPEC-193, listed so that record stays valid; new passes do not write it.
review_gate.pass no integer ≥ 1 11 This branch's review passes so far, +1 each time (SPEC-192).
review_gate.reviewers_unmeasured · .reviewers_deferred · .reviewers_failed (all array only) · .reviewers_outstanding (array, or a legacy integer) no array of names; reviewers_outstanding additionally accepts an integer [], [security-sentinel] SPEC-192 FR-7: the per-reviewer record must survive the gitignored gate file. These are the FOUR uncovered states, each of which forces completeness: INCOMPLETE — unmeasured returned without executing anything, outstanding has not returned, deferred was dispatched and then stood down, failed errored or hit Step 2's time bound. ⚠ deferred and failed were absent here until pass 14, so the honest record naming them was REJECTED (additional-property) while the record that drops the names was accepted: FR-7's axis held for two states and was lost for the other two, and /vt-d-complete read two keys that could not exist in the file it reads. An integer is the pre-SPEC-192 count form, accepted only so dated records (SPEC-170, SPEC-183) stay valid without being rewritten, and only for outstanding.
review_gate.degraded_reason no string security-sentinel could not run the suite Why the pass is INCOMPLETE, verbatim (SPEC-192 pass 6 H-7). Omitted when nothing is uncovered.
finalize_gate.date · .version no date · string 2026-09-24, 6.4.0 When and which version.
finalize_gate.checks_passed · .checks_failed no integer ≥ 0 14, 0 Finalize check counts.

gate_bypass records gates skipped on purpose:

Field Required Type / values Example Meaning
gate_bypass.date yes date 2026-05-06 When the gates were skipped.
gate_bypass.reason yes string cross-repo duplicate Why.
gate_bypass.skipped_gates no list of strings [plan_gate, build_gate] Which gates.

gate_history

A gate object always holds its latest pass — that is the block /vt-d-complete reads. When /vt-d-4-review runs again, the review_gate pass it replaces moves here, and the new pass is written into review_gate (D12). Only the fields listed below move as fields — and the list below is the whole of it, including the six SPEC-192 audit fields (completeness, reviewers_unmeasured, reviewers_outstanding, reviewers_deferred, reviewers_failed, degraded_reason). Until pass 13 this table omitted the first four while this sentence claimed to be exhaustive, so a writer following the page demoted completeness into prose and destroyed the FR-7 axis on every move. ⚠ And this sentence then said "four" for a day after pass 14 added the last two rows to the table below (pass 14, data-integrity M-2) — the inverse of the same defect, two lines from where it is recorded. A10 cannot catch it: it only checks that the page NAMES every schema field, which the new rows do, so a parenthetical that under-counts the table it introduces is invisible to it. Count the rows below before trusting this list. Anything NOT listed below (legacy pass_number, note, a list-valued notes, …) becomes field: value lines in the entry's notes (D16, rule in /vt-d-4-review Step 6c). A pass is never recorded as a new top-level key (review_gate_pass_2, remediation_pass_6, … are violations).

Scope today: the history item carries the review gate's audit fields (below). The plan, build and finalize writers still overwrite their block on a re-run, and their gate-specific fields (plan_gate.artifacts, build_gate.verification, finalize_gate.version, …) have no place in a history item yet; a skipped shape_gate has no status to record. Items:

Field Required Type / values Example Meaning
gate yes spec_check_gate, shape_gate, plan_gate, build_gate, test_gate, review_gate, finalize_gate review_gate Which gate.
pass yes integer ≥ 1 2 Pass number.
status yes gate vocabulary value PASS Verdict of this pass.
date no date 2026-09-25 When.
branch · head no string · commit id (quoted) feature/x, "b1e590ad5" What this pass judged.
critical · high · medium · low no integer ≥ 0 0, 1 Findings left open after this pass, per severity.
validator · validators_run no agent or inline · list of strings agent, [security-sentinel] Who produced the verdict (SPEC-189).
completeness no COMPLETE, INCOMPLETE, COMPLETE_FOR_REDUCED_ROSTER COMPLETE SPEC-192 FR-7: whether THAT pass finished. Moves as a FIELD, never into notes — demoting it to prose destroys the only machine-readable record that the pass was degraded.
reviewers_unmeasured no array of names [], [security-sentinel] SPEC-192 FR-7: returned without executing anything. Array only — no corpus record carries the integer form.
reviewers_outstanding no array of names, or a legacy integer count [], 2 SPEC-192 FR-7: dispatched and never returned. The integer is the pre-SPEC-192 count form. ⚠ This is the gate_history row, and NO gate_history item in the corpus carries the integer — re-measured 2026-10-06 over specs/*/state.yaml: 2 hits, both in review_gate (SPEC-170, SPEC-183), 0 in history. The integer is admitted here for the day one of those review_gate blocks is MOVED into history by Step 6c, which is the one path that would otherwise reject a record for being faithfully archived. Until pass 14 this row said it was "kept so SPEC-183's dated record stays valid", which named a record that is not in history (document-quality Low); the sibling reviewers_unmeasured row correctly says no corpus record carries the integer.
reviewers_deferred no array of names [], [angular-reviewer] SPEC-192 FR-7: dispatched and then stood down for this pass — operationally, repeatedly unschedulable on capacity. workflow-4-review Step 2.9 is the single definition; this row and the schema description defer to it. ⚠ Never "not dispatched": the five lists partition reviewers, which IS the dispatched set (pass 14, M7). Array only — there is no legacy integer form, because the key could not be written at all until pass 14.
reviewers_failed no array of names [], [security-sentinel] SPEC-192 FR-7: errored, or stopped at Step 2's 30-minute bound. Array only — see reviewers_deferred.
degraded_reason no string security-sentinel could not run the suite Why that pass was INCOMPLETE, verbatim.
notes no string closed H1, H2 What the pass found or fixed.

status takes the gate vocabulary only, so a spec_check_gate re-run (whose status is RUN) cannot be recorded here today.

gate_history:
  - gate: review_gate
    pass: 1
    status: FAIL
    date: "2026-09-25"
    validator: agent
    branch: feature/spec-212-spec-json-schema
    head: "b1e590ad5"
    critical: 0
    high: 2
    completeness: COMPLETE
    reviewers_unmeasured: []
    notes: "pass 1; review_gate now holds pass 2"

.design-state.yaml

The file may contain current_phase, specs_directory, specs_status, bugchen_status and the deprecated specchen_status. Bug records under bugchen_status are out of scope and checked only as an object — but YAML-level errors (duplicate-key, load-error) are reported anywhere in the file, and a load-error stops the whole registry check.

Field Required Type / values Example Meaning
current_phase no string development Workflow phase of the repository.
specs_directory no string specs/ Directory holding the spec folders.
specs_status no spec id → entry see below The spec registry.
bugchen_status no object — Bug registry (opaque here).
specchen_status no, deprecated object — Old name of specs_status.

Registry entry (specs_status.SPEC-NNN)

Required: file, status. activated_at/completed_at never go here — they live in state.yaml.

Field Required Type / values Example Meaning
file yes string 212-json-schema-for-the-spec-structure/spec.md spec.md path relative to specs_directory.
status yes spec lifecycle value in_progress Mirrors state.yaml (guard 54).
priority no P0 … P3 P1 Delivery priority.
dependencies no list of spec ids [] Blocking dependencies (the registry keeps this key).
specs_dir no specs/<dir>/ — <dir> starts with a letter or digit, then letters, digits, ., _ or -; so never . or .. specs/212-json-schema-for-the-spec-structure/ Spec directory.
dir no, deprecated specs/<dir>/, same rule as specs_dir — Old name of specs_dir.
title no string JSON Schema for the spec structure Title, read by the domain dashboard.
deliverable_types no non-empty list of deliverable types [code] Read by guard 58 (scripts/tests/setup/58-deliverable-types-are-declared.sh).
completed_date no date 2026-09-30 When the spec was completed.
specs_status:
  SPEC-212:
    file: 212-json-schema-for-the-spec-structure/spec.md
    status: in_progress
    priority: P1
    dependencies: []
    specs_dir: specs/212-json-schema-for-the-spec-structure/

Vocabularies

The status enums are mirrors of plugins/vt-base/lib/status-vocabulary.tsv (SPEC-181); test-spec-schema-vocab-drift.sh fails on any difference in either direction.

Vocabulary Values Used by
spec lifecycle (spec) proposed, specified, in_progress, completed, archiviert, parked, rejected, withdrawn state.yaml status, registry status, deprecated frontmatter status
gate (gate) COMPLETE, PASS, GO, GO WITH CAUTION, FAIL, NO-GO, PENDING, SKIPPED, PASS_WITH_UNVALIDATED_REVISION, PASS_WAVE1_SCOPE; alias GO-WITH-CAUTION → GO WITH CAUTION every gate status except spec_check_gate, plus gate_history[].status
spec check (spec-check) RUN spec_check_gate.status only
deliverable types code, test, research, document (aliases documents, documentation, docs, doc), presentation (presentations), project-plan (project-plans) deliverable_types — the tokens of persona-select.sh

Write the canonical form (GO WITH CAUTION, document); the aliases are accepted because the vocabulary accepts them.

spec.md body sections

In this order (canonical source: /vt-d-spec-from-requirements Step 3). Include only sections with real content — never placeholder text. The body is documented, not schema-checked; /vt-d-spec-check checks its quality.

  1. # SPEC-NNN: <Title>
  2. ## Problem — the observed issue or need
  3. ## Goal — the desired outcome
  4. ## User Stories — with Given/When/Then acceptance criteria
  5. ## Functional Requirements — table ID | Requirement
  6. ## Visual Reference — screenshots, mockups or a text description (may be omitted for ui_scope: backend)
  7. ## Non-Functional Requirements
  8. ## Edge Cases — table ID | Edge Case | Expected Behavior
  9. ## Scope — In Scope / Out of Scope
  10. ## Dependencies
  11. ## Success Criteria — numbered, measurable
  12. ## Definition of Ready — the canonical checklist
  13. ## Definition of Done — the canonical checklist
  14. ## Sibling-Candidates — appended by /vt-d-spec-from-requirements only when similar specs survive its sibling check (SPEC-132)

Validating a spec

plugins/vt-base/bin/spec-schema-check specs/212-json-schema-for-the-spec-structure
plugins/vt-base/bin/spec-schema-check --registry
plugins/vt-base/bin/spec-schema-check --all

Run it from the repository root, or pass --root. Each violation is one line of five tab-separated fields — directory<TAB>file<TAB>JSON-pointer<TAB>code<TAB>message — sorted (LC_ALL=C). directory is . for the registry; an empty pointer means the whole document. A backslash, or a control, format (bidi, zero-width) or line-separator character, inside a field is written as \\, \xNN, \uNNNN or \UNNNNNNNN, so a field never spans a tab or a line.

Exit 0 means clean, 1 violations, 2 a usage error, a schema file the validator cannot enforce, or an internal fault — a crash never reads as "violations". The validator is standard-library Python (no PyYAML); it reads the YAML subset spec files use and reports anything outside it as load-error instead of guessing.

Option Meaning
--all · --registry · <spec-dir>… What to check: everything, only .design-state.yaml, or the named spec directories. Exactly one.
--root DIR Repository root (default: the current directory).
--schema-dir DIR Schema files (default: plugins/vt-base/lib/schema).
--baseline-format Print only the four key fields (the baseline row format).
--summary Also print # specs=N documents=M load_errors=K on stderr.
-h, --help Usage.
Code Meaning
additional-property A field the schema does not define.
required A required field is missing (the pointer names it).
type, enum, pattern, format, min-length, min-items, unique-items, minimum, maximum A value breaks the rule of the same name.
missing-frontmatter spec.md has no --- frontmatter block.
missing-file spec.md, state.yaml or the registry is absent, or not a regular file. The name must match exactly: SPEC.md does not count as spec.md, on any filesystem.
symlink A spec directory, spec.md, state.yaml or the registry is a symlink, or a spec directory lies outside the root (a link anywhere on its path, a .. step, or an absolute path outside it); it is not followed and nothing is read. A symlinked specs/ itself is one row, . / specs.
load-error The file is not valid UTF-8, or the YAML uses a construct outside the supported subset, or one PyYAML refuses: anchors, aliases, tags, any TAB (quoted or not, D17), a --- inside the document or a delimiter written ---, a plain = or << value, an unquoted impossible date or time (2026-02-31 — quoted, it is a format violation instead), a number that cannot be built, lone CR/NEL/U+2028/U+2029, control characters, nesting deeper than 64 levels, …. The message names the line and the construct.
duplicate-key A key appears twice in one mapping; the last value is validated.
registry-orphan A registry entry points to no spec directory.
unregistered-spec A spec directory whose id has no registry entry.

Baseline and ratchet

Guard 126 (scripts/tests/setup/126-spec-schema-ratchet.sh) runs spec-schema-check --all and compares the result with scripts/tests/setup/spec-schema-baseline.txt, one tab-separated row directory<TAB>file<TAB>JSON-pointer<TAB>code per deviation that existed when the schemas landed.

  • A violation that is not in the baseline fails the suite — a new spec, or a new field on an old one, must match the schema.
  • A baseline row that no longer matches a violation fails too — when you fix an old spec, delete its row in the same commit. The failure message names the row.
  • The baseline only shrinks: rows added against the merge base fail, and on main a merge commit is compared with the main tip before it. Merge a branch into main as a merge or squash commit — a fast-forward or GitHub's "Rebase and merge" would let added rows through.
  • Never hand-merge the baseline. On a merge conflict — or after any merge that touches specs/ — regenerate it from the merged tree, under the file's comment header:
plugins/vt-base/bin/spec-schema-check --all --baseline-format | LC_ALL=C sort -u

The result must be a subset of the previous rows. A new row means the merged branch deviates: fix that spec (ad-hoc review passes move into gate_history), do not keep the row.

To change the structure itself, change the schema, specs/212-json-schema-for-the-spec-structure/data-model.md and this page together; the schema tests check the enums against the vocabulary and the /vt-d-spec-from-requirements and /vt-d-specs-from-prd templates against the schemas.