Skip to content

[Proposal/Enhancement] Hardcoded spec format prevents custom schemas from working #666

@lsmonki

Description

@lsmonki

Problem

OpenSpec hardcodes spec structure expectations (## Requirements, ### Requirement:, #### Scenario: inline) throughout parsers, validators, and generators. This means custom schemas that define different formats (e.g., ## Functional Requirements, scenarios in separate verify.md files) fail validation even when their specs are correctly structured according to their own schema.

This prevents OpenSpec from working with projects that have pre-existing specs in different formats. Real-world scenarios include:

  • Teams with specs created before adopting OpenSpec
  • Specs generated by other tools
  • Human-written specs following company/team conventions
  • Custom schemas that use different headers (e.g., ## Functional Requirements) or separate verification files

OpenSpec should adapt to existing spec formats, not impose its own.

Root cause

The spec format is embedded in code, not read from the schema. The schema defines the workflow (artifacts), but OpenSpec ignores it when parsing and validating specs.

Expected behavior

Schemas should be able to define their spec structure via artifact configuration. OpenSpec should read this configuration and adapt its parsing, validation, and generation accordingly.

Proposal

Configurable Spec Format

Why

OpenSpec currently hardcodes spec structure expectations throughout the codebase:

  • ## Requirements as the requirements section header
  • ### Requirement: {name} as the requirement header pattern
  • #### Scenario: {name} as inline scenario headers
  • Scenarios expected inline within spec.md

What Changes

  • schema.yaml: Extend schema with validation config (changeValidation, specValidation) and artifact fields for spec structure
  • Parsers: Make markdown-parser.ts and requirement-blocks.ts configurable based on schema
  • Validators: Pass spec format configuration to validation logic
  • Generators: Use schema's spec format when generating new specs and deltas
  • Sync/Archive: Respect configured format when merging deltas to main specs

Capabilities

Modified Capabilities

  • artifact-graph: Extend artifact schema with new optional fields for spec structure configuration
  • cli-validate: Validation must use artifact-defined spec format instead of hardcoded expectations

Impact

Code Areas

Parsers (high impact):

  • src/core/parsers/markdown-parser.ts - section detection
  • src/core/parsers/requirement-blocks.ts - requirement extraction, delta parsing
  • src/core/parsers/change-parser.ts - rename format parsing

Validation (high impact):

  • src/core/validation/validator.ts - spec validation rules
  • src/core/validation/constants.ts - error messages
  • src/commands/validate.ts - user guidance, hardcoded spec.md path
  • src/commands/change.ts - help messages with hardcoded format

Discovery (medium impact):

  • src/utils/item-discovery.ts - getSpecIds() only looks for spec.md, needs to read all files per schema

Generation/Sync (high impact):

  • src/core/specs-apply.ts - skeleton generation, delta merge
  • src/core/templates/skill-templates.ts - LLM prompts contain hardcoded delta format (## ADDED Requirements, etc.), scenario patterns (#### Scenario:), and requirement patterns. These prompts tell the AI how to generate specs.
  • src/commands/schema.ts - hardcoded example spec format

Schema system (medium impact):

  • src/core/artifact-graph/types.ts - extend ArtifactSchema with new fields
  • src/core/artifact-graph/schema.ts - load new artifact fields

Templates (medium impact):

  • schemas/spec-driven/templates/spec.md - hardcoded format, should use schema config
  • schemas/spec-driven/schema.yaml - add explicit default values for new artifact fields

Breaking Changes

None. All new fields are optional with defaults that maintain backward compatibility. Existing schemas work without modification.


Design Considerations

Three Levels of Format

There are three distinct format concerns:

┌────────────┐      ┌────────────┐      ┌────────────┐
│  Project   │ ──►  │  Internal  │ ──►  │  Project   │
│  Format    │ READ │  Format    │WRITE │  Format    │
│  (input)   │      │ (canonical)│      │  (output)  │
└────────────┘      └────────────┘      └────────────┘
  1. Project Format (input): How specs exist in the project - diverse formats created by humans, other tools, or legacy systems. OpenSpec must READ these.

  2. Internal Format (canonical): OpenSpec's internal representation. This is the current hardcoded format (## Requirements, ### Requirement:, etc.). This stays fixed for backward compatibility.

  3. Project Format (output): How OpenSpec WRITES new specs, deltas, and modifications. Must match the project's expected format.

Key insight: The internal format doesn't change. What changes is:

  • How we MAP from project format → internal format (parsing)
  • How we MAP from internal format → project format (generation)

This maintains full backward compatibility - projects using the default spec-driven schema see no changes.

Spec as a Collection of Files

A "spec" is not just one file. It's a collection that varies by schema:

schema-a:               spec-driven:          schema-c:
<capability>/           <capability>/         <capability>/
├── spec.md (required)  └── spec.md           ├── requirements.md
├── verify.md (required)    (scenarios        ├── verification.md
└── .metadata (opt)          inline)          └── examples/

Elements That Need Configuration

Section headers:

  • Main requirements section: ## Requirements vs ## Functional Requirements vs ## Requisitos
  • Purpose section: ## Purpose vs ## Overview vs custom

Requirement identification:

  • Pattern: ### Requirement: {name} vs ### Req: {name} vs ## RF-001: {name}

Scenarios/Verification:

  • Location: inline in spec.md, separate file (e.g., verify.md), or none
  • Header pattern: #### Scenario: vs ### Scenario: vs Given/When/Then blocks
  • Required or optional
  • Note: Current validation assumes scenarios are inline in spec.md. Other schemas may have verification in separate files or not at all. Validation must respect this.

Delta format:

  • Section suffix: ADDED Requirements vs ADDED Functional Requirements
  • Whether deltas use same header as main spec or fixed format

File composition:

  • Which files make up a complete spec
  • Which are required vs optional

Schema Structure: Before and After

BEFORE (current schema.yaml):

name: custom-schema
version: 1
description: "..."

artifacts:
  - id: proposal
    generates: proposal.md
    template: proposal.md
    instruction: |
      ...
    requires: []

  - id: specs
    generates: "specs/**/*.md"
    template: spec.md
    instruction: |
      ...
    requires: [proposal]

  # ... more artifacts

apply:
  requires: [tasks]
  tracks: tasks.md

The schema defines the workflow (artifacts), but spec structure is hardcoded in OpenSpec's parsers and validators.


AFTER (extended schema with validation config):

name: custom-schema
version: 1
description: "..."

# NEW: Validation configuration at schema level
changeValidation:
  artifact: "verify"                         # which artifact verifies changes

specValidation:
  artifact: "spec-verify"                    # which artifact contains scenarios
  pattern: "### Scenario: {name}"            # how to identify scenarios
  required: true                             # whether scenarios are mandatory

artifacts:
  - id: proposal
    generates: proposal.md
    template: proposal.md
    instruction: |
      ...
    requires: []

  - id: specs
    generates: "specs/**/*.md"
    template: spec.md
    instruction: |
      ...
    requires: [proposal]

    # NEW: Extended artifact configuration (structure only, no scenarios)
    sections:
      required: ["Purpose", "Functional Requirements"]
      optional: ["Scope", "Status", "Definitions", "Constraints"]
      requirement:
        section: "Functional Requirements"     # which section contains requirements
        pattern: "### Requirement: {name}"     # how to identify requirements

  - id: spec-verify
    generates: "specs/**/verify.md"            # output artifact: synced to specs/
    template: spec-verify.md
    instruction: |
      ...
    requires: [specs]
    sections:
      required: ["Acceptance Criteria", "Test Scenarios"]
      optional: ["Verification Methods", "Failure Conditions"]

  - id: verify
    generates: verify.md                       # workflow artifact: NOT synced
    template: verify.md
    instruction: |
      ...
    requires: [specs]
    # Referenced by changeValidation.artifact

  - id: design
    generates: design.md
    template: design.md
    requires: [proposal]

  - id: tasks
    generates: tasks.md
    template: tasks.md
    requires: [specs, design]

apply:
  requires: [tasks]
  tracks: tasks.md

Two new schema-level blocks (changeValidation, specValidation) plus extended artifact fields for structure.

All new fields are optional. When omitted, OpenSpec uses defaults that match current spec-driven behavior. Existing schemas work without modification.

New Schema Fields Specification

The following fields are added to the schema. All fields are optional with backward-compatible defaults—existing schemas continue to work without modification.

Schema-Level Validation Config

# New schema-level fields
changeValidation:
  artifact: string                      # Which artifact verifies change implementation

specValidation:
  artifact: string                      # Which artifact contains scenarios (e.g., "specs" or "spec-verify")
  pattern: string                       # Pattern to identify scenarios (e.g., "#### Scenario: {name}")
  required: boolean                     # Whether scenarios are mandatory
  shallMustPattern: string | null       # Regex pattern for normative keywords (e.g., "SHALL|MUST"), null to disable

Artifact-Level Structure Config

# New fields for artifact definition
artifacts:
  - id: string              # existing
    generates: string       # existing
    template: string        # existing
    instruction: string     # existing (optional)
    requires: string[]      # existing

    # NEW FIELDS:

    sections:                           # Section configuration
      required: string[]                # Sections that MUST exist (e.g., ["Purpose", "Requirements"])
      optional: string[]                # Sections that MAY exist (for documentation)
      requirement:                      # Requirement block configuration (only for spec artifacts)
        section: string                 # Which section contains requirements (e.g., "Functional Requirements")
        pattern: string                 # Pattern to identify requirements (e.g., "### Requirement: {name}")

Field Details:

Field Type Default Description
changeValidation.artifact string "verify" Artifact that verifies change implementation
specValidation.artifact string "specs" Artifact containing scenarios ("specs" = inline)
specValidation.pattern string "#### Scenario: {name}" Pattern to identify scenario blocks
specValidation.required boolean true Whether validation fails if no scenarios found
specValidation.shallMustPattern string | null "SHALL|MUST" Regex pattern for normative keywords; null or empty to disable
sections.required string[] ["Purpose", "Requirements"] Section headers that must exist
sections.optional string[] [] Section headers that are recognized but optional
sections.requirement.section string "Requirements" Section containing requirements (used for delta operations)
sections.requirement.pattern string "### Requirement: {name}" Pattern to identify requirement blocks

shallMustPattern Examples:

# Default: strict uppercase (RFC 2119 style)
shallMustPattern: "SHALL|MUST"
# Matches: "The system SHALL validate input"
# Matches: "Users MUST authenticate"
# Fails:   "The system should validate input"

# Case-insensitive: accepts lowercase variants
shallMustPattern: "[Ss][Hh][Aa][Ll][Ll]|[Mm][Uu][Ss][Tt]"
# Matches: "The system shall validate input"
# Matches: "Users must authenticate"
# Matches: "The system SHALL validate input"

# Extended: include SHOULD for less strict requirements
shallMustPattern: "SHALL|MUST|SHOULD"
# Matches: "The system SHOULD log errors"

# Spanish: for Spanish-language specifications
shallMustPattern: "DEBE|DEBERÁ|TIENE QUE"
# Matches: "El sistema DEBE validar la entrada"

# Disabled: no normative keyword validation
shallMustPattern: null
# or
shallMustPattern: ""
# All requirements pass regardless of wording

Delta sections are derived from sections.requirement.section:

  • If sections.requirement.section: "Functional Requirements", deltas use ## ADDED Functional Requirements, ## MODIFIED Functional Requirements, etc.

Workflow vs Output Artifacts

The current artifact system already supports distinguishing between:

  1. Workflow artifacts (temporary, for the change process)
  2. Output artifacts (permanent, synced to specs/)

Example:

artifacts:
  - id: verify
    generates: verify.md
    # Workflow artifact: verifies the change implementation
    # Lives at: <change>/verify.md
    # NOT synced to specs/

  - id: spec-verify
    generates: "specs/**/verify.md"
    # Output artifact: verification for each capability
    # Lives at: <change>/specs/<cap>/verify.md
    # Synced to: specs/<scope>/<cap>/verify.md

The generates pattern determines behavior:

  • generates: verify.md → single file at change root (workflow)
  • generates: "specs/**/verify.md" → files alongside specs (output, synced)

Change Validation vs Spec Validation

Two distinct validation concerns, configured at schema level:

# How changes are verified (during development)
changeValidation:
  artifact: "verify"                   # uses <change>/verify.md

# How specs are validated (structure validation)
specValidation:
  artifact: "specs"                    # scenarios in spec.md (inline)
  pattern: "#### Scenario: {name}"
  required: true

Or with separate verification files:

changeValidation:
  artifact: "verify"                   # uses <change>/verify.md

specValidation:
  artifact: "spec-verify"              # scenarios in verify.md (separate)
  pattern: "### Scenario: {name}"
  required: true

Key distinction:

  • changeValidation → which artifact verifies that implementation matches change requirements
  • specValidation → which artifact contains scenarios for validating spec structure

Reading Specs: All Files, Not Just spec.md

Currently OpenSpec treats specs as directories (openspec/specs/{id}/) but only reads spec.md (hardcoded in validate.ts:142,209). Other files in the directory are ignored.

With this change, OpenSpec reads all files in the spec directory, matching them to output artifacts defined in the schema.

When reading/validating specs, OpenSpec should:

  1. Scan the spec directory for all files
  2. Match files to artifacts based on generates patterns
  3. Validate presence of required files (per artifact configuration)
  4. Parse each file according to its artifact's configuration

Example for a schema with specs and spec-verify artifacts:

specs/user-auth/
├── spec.md        ← matched by specs artifact (generates: "specs/**/*.md")
└── verify.md      ← matched by spec-verify artifact (generates: "specs/**/verify.md")

OpenSpec validates:

  • spec.md exists (required by specs artifact)
  • verify.md exists (required by spec-verify artifact)
  • Each file has its required sections per artifact config

Dynamic Prompts (Schema-Aware Skills)

Skills are defined once in skill-templates.ts, but each change can use a different schema. Prompts must dynamically read format configuration instead of hardcoding values.

Current approach (hardcoded):

Use these delta sections:
- `## ADDED Requirements`
- `## MODIFIED Requirements`

New approach (schema-aware):

Before generating specs, read the schema configuration:

1. Find the change's schema: `openspec/changes/<name>/.openspec.yaml``schema` field
2. Load schema definition: `schemas/<schema>/schema.yaml` or `openspec/schemas/<schema>/schema.yaml`
3. Read schema-level validation config:
   - `specValidation.artifact` → which artifact has scenarios
   - `specValidation.pattern` → how to format scenario headers
   - `specValidation.required` → whether scenarios are mandatory
4. Find the `specs` artifact and read its format fields:
   - `sections.requirement.section` → use for delta headers (e.g., "Functional Requirements" → `## ADDED Functional Requirements`)
   - `sections.requirement.pattern` → how to format requirement headers

If fields are not defined, use defaults:
- `specValidation.artifact`: "specs" (inline)
- `specValidation.pattern`: "#### Scenario: {name}"
- `specValidation.required`: true
- `sections.requirement.section`: "Requirements"
- `sections.requirement.pattern`: "### Requirement: {name}"

This approach:

  • Works with existing skill architecture (no code changes to skill loading)
  • Lets the AI adapt to any schema at runtime
  • Maintains backward compatibility (defaults match current behavior)

Default Values (backward compatible)

When extended fields are omitted, OpenSpec uses these defaults (matching current spec-driven behavior):

# Default schema-level validation config
changeValidation:
  artifact: "verify"

specValidation:
  artifact: "specs"                      # scenarios inline in spec.md
  pattern: "#### Scenario: {name}"
  required: true
  shallMustPattern: "SHALL|MUST"         # regex pattern for normative keywords (null to disable)

# Default for specs artifact
artifacts:
  - id: specs
    sections:
      required: ["Purpose", "Requirements"]
      requirement:
        section: "Requirements"
        pattern: "### Requirement: {name}"

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions