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) │
└────────────┘ └────────────┘ └────────────┘
-
Project Format (input): How specs exist in the project - diverse formats created by humans, other tools, or legacy systems. OpenSpec must READ these.
-
Internal Format (canonical): OpenSpec's internal representation. This is the current hardcoded format (## Requirements, ### Requirement:, etc.). This stays fixed for backward compatibility.
-
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:
- Workflow artifacts (temporary, for the change process)
- 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:
- Scan the spec directory for all files
- Match files to artifacts based on
generates patterns
- Validate presence of required files (per artifact configuration)
- 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}"
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:
## Functional Requirements) or separate verification filesOpenSpec 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:
## Requirementsas the requirements section header### Requirement: {name}as the requirement header pattern#### Scenario: {name}as inline scenario headersWhat Changes
changeValidation,specValidation) and artifact fields for spec structuremarkdown-parser.tsandrequirement-blocks.tsconfigurable based on schemaCapabilities
Modified Capabilities
Impact
Code Areas
Parsers (high impact):
src/core/parsers/markdown-parser.ts- section detectionsrc/core/parsers/requirement-blocks.ts- requirement extraction, delta parsingsrc/core/parsers/change-parser.ts- rename format parsingValidation (high impact):
src/core/validation/validator.ts- spec validation rulessrc/core/validation/constants.ts- error messagessrc/commands/validate.ts- user guidance, hardcoded spec.md pathsrc/commands/change.ts- help messages with hardcoded formatDiscovery (medium impact):
src/utils/item-discovery.ts-getSpecIds()only looks for spec.md, needs to read all files per schemaGeneration/Sync (high impact):
src/core/specs-apply.ts- skeleton generation, delta mergesrc/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 formatSchema system (medium impact):
src/core/artifact-graph/types.ts- extend ArtifactSchema with new fieldssrc/core/artifact-graph/schema.ts- load new artifact fieldsTemplates (medium impact):
schemas/spec-driven/templates/spec.md- hardcoded format, should use schema configschemas/spec-driven/schema.yaml- add explicit default values for new artifact fieldsBreaking 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 Format (input): How specs exist in the project - diverse formats created by humans, other tools, or legacy systems. OpenSpec must READ these.
Internal Format (canonical): OpenSpec's internal representation. This is the current hardcoded format (
## Requirements,### Requirement:, etc.). This stays fixed for backward compatibility.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:
This maintains full backward compatibility - projects using the default
spec-drivenschema see no changes.Spec as a Collection of Files
A "spec" is not just one file. It's a collection that varies by schema:
Elements That Need Configuration
Section headers:
## Requirementsvs## Functional Requirementsvs## Requisitos## Purposevs## Overviewvs customRequirement identification:
### Requirement: {name}vs### Req: {name}vs## RF-001: {name}Scenarios/Verification:
#### Scenario:vs### Scenario:vs Given/When/Then blocksDelta format:
ADDED RequirementsvsADDED Functional RequirementsFile composition:
Schema Structure: Before and After
BEFORE (current schema.yaml):
The schema defines the workflow (artifacts), but spec structure is hardcoded in OpenSpec's parsers and validators.
AFTER (extended schema with validation config):
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-drivenbehavior. 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
Artifact-Level Structure Config
Field Details:
changeValidation.artifactstring"verify"specValidation.artifactstring"specs""specs"= inline)specValidation.patternstring"#### Scenario: {name}"specValidation.requiredbooleantruespecValidation.shallMustPatternstring | null"SHALL|MUST"nullor empty to disablesections.requiredstring[]["Purpose", "Requirements"]sections.optionalstring[][]sections.requirement.sectionstring"Requirements"sections.requirement.patternstring"### Requirement: {name}"shallMustPatternExamples:Delta sections are derived from
sections.requirement.section: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:
Example:
The
generatespattern 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:
Or with separate verification files:
Key distinction:
changeValidation→ which artifact verifies that implementation matches change requirementsspecValidation→ which artifact contains scenarios for validating spec structureReading Specs: All Files, Not Just spec.md
Currently OpenSpec treats specs as directories (
openspec/specs/{id}/) but only readsspec.md(hardcoded invalidate.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:
generatespatternsExample for a schema with
specsandspec-verifyartifacts:OpenSpec validates:
spec.mdexists (required by specs artifact)verify.mdexists (required by spec-verify artifact)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):
New approach (schema-aware):
This approach:
Default Values (backward compatible)
When extended fields are omitted, OpenSpec uses these defaults (matching current
spec-drivenbehavior):