mirror of
https://github.com/Fission-AI/OpenSpec.git
synced 2026-10-04 06:18:24 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53e137aca6 | ||
|
|
63b25a5dda | ||
|
|
ee517e5fd9 | ||
|
|
bfd8925495 |
@@ -0,0 +1,211 @@
|
||||
# Schema Customization
|
||||
|
||||
This document describes how users can customize OpenSpec schemas and templates, the current manual process, and the gap that needs to be addressed.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
OpenSpec uses a 2-level schema resolution system following the XDG Base Directory Specification:
|
||||
|
||||
1. **User override**: `${XDG_DATA_HOME}/openspec/schemas/<name>/`
|
||||
2. **Package built-in**: `<npm-package>/schemas/<name>/`
|
||||
|
||||
When a schema is requested (e.g., `spec-driven`), the resolver checks the user directory first. If found, that entire schema directory is used. Otherwise, it falls back to the package's built-in schema.
|
||||
|
||||
---
|
||||
|
||||
## Current Manual Process
|
||||
|
||||
To override the default `spec-driven` schema, a user must:
|
||||
|
||||
### 1. Determine the correct directory path
|
||||
|
||||
| Platform | Path |
|
||||
|----------|------|
|
||||
| macOS/Linux | `~/.local/share/openspec/schemas/` |
|
||||
| Windows | `%LOCALAPPDATA%\openspec\schemas\` |
|
||||
| All (if set) | `$XDG_DATA_HOME/openspec/schemas/` |
|
||||
|
||||
### 2. Create the directory structure
|
||||
|
||||
```bash
|
||||
# macOS/Linux example
|
||||
mkdir -p ~/.local/share/openspec/schemas/spec-driven/templates
|
||||
```
|
||||
|
||||
### 3. Find and copy the default schema files
|
||||
|
||||
The user must locate the installed npm package to copy the defaults:
|
||||
|
||||
```bash
|
||||
# Find the package location (varies by install method)
|
||||
npm list -g openspec --parseable
|
||||
# or
|
||||
which openspec && readlink -f $(which openspec)
|
||||
|
||||
# Copy files from the package's schemas/ directory
|
||||
cp <package-path>/schemas/spec-driven/schema.yaml ~/.local/share/openspec/schemas/spec-driven/
|
||||
cp <package-path>/schemas/spec-driven/templates/*.md ~/.local/share/openspec/schemas/spec-driven/templates/
|
||||
```
|
||||
|
||||
### 4. Modify the copied files
|
||||
|
||||
Edit `schema.yaml` to change the workflow structure:
|
||||
|
||||
```yaml
|
||||
name: spec-driven
|
||||
version: 1
|
||||
description: My custom workflow
|
||||
artifacts:
|
||||
- id: proposal
|
||||
generates: proposal.md
|
||||
description: Initial proposal
|
||||
template: proposal.md
|
||||
requires: []
|
||||
# Add, remove, or modify artifacts...
|
||||
```
|
||||
|
||||
Edit templates in `templates/` to customize the content guidance.
|
||||
|
||||
### 5. Verify the override is active
|
||||
|
||||
Currently there's no command to verify which schema is being used. Users must trust that the file exists in the right location.
|
||||
|
||||
---
|
||||
|
||||
## Gap Analysis
|
||||
|
||||
The current process has several friction points:
|
||||
|
||||
| Issue | Impact |
|
||||
|-------|--------|
|
||||
| **Path discovery** | Users must know XDG conventions and platform-specific paths |
|
||||
| **Package location** | Finding the npm package path varies by install method (global, local, pnpm, yarn, volta, etc.) |
|
||||
| **No scaffolding** | Users must manually create directories and copy files |
|
||||
| **No verification** | No way to confirm which schema is actually being resolved |
|
||||
| **No diffing** | When upgrading openspec, users can't see what changed in built-in templates |
|
||||
| **Full copy required** | Must copy entire schema even to change one template |
|
||||
|
||||
### User Stories Not Currently Supported
|
||||
|
||||
1. *"I want to add a `research` artifact before `proposal`"* — requires manual copy and edit
|
||||
2. *"I want to customize just the proposal template"* — must copy entire schema
|
||||
3. *"I want to see what the default schema looks like"* — must find package path
|
||||
4. *"I want to revert to defaults"* — must delete files and hope paths are correct
|
||||
5. *"I upgraded openspec, did the templates change?"* — no way to diff
|
||||
|
||||
---
|
||||
|
||||
## Proposed Solution: Schema Configurator
|
||||
|
||||
A CLI command (or set of commands) that handles path resolution and file operations for users.
|
||||
|
||||
### Option A: Single `openspec schema` command
|
||||
|
||||
```bash
|
||||
# List available schemas (built-in and user overrides)
|
||||
openspec schema list
|
||||
|
||||
# Show where a schema resolves from
|
||||
openspec schema which spec-driven
|
||||
# Output: /Users/me/.local/share/openspec/schemas/spec-driven/ (user override)
|
||||
# Output: /usr/local/lib/node_modules/openspec/schemas/spec-driven/ (built-in)
|
||||
|
||||
# Copy a built-in schema to user directory for customization
|
||||
openspec schema copy spec-driven
|
||||
# Creates ~/.local/share/openspec/schemas/spec-driven/ with all files
|
||||
|
||||
# Show diff between user override and built-in
|
||||
openspec schema diff spec-driven
|
||||
|
||||
# Remove user override (revert to built-in)
|
||||
openspec schema reset spec-driven
|
||||
|
||||
# Validate a schema
|
||||
openspec schema validate spec-driven
|
||||
```
|
||||
|
||||
### Option B: Dedicated `openspec customize` command
|
||||
|
||||
```bash
|
||||
# Interactive schema customization
|
||||
openspec customize
|
||||
# Prompts: Which schema? What do you want to change? etc.
|
||||
|
||||
# Copy and open for editing
|
||||
openspec customize spec-driven
|
||||
# Copies to user dir, prints path, optionally opens in $EDITOR
|
||||
```
|
||||
|
||||
### Option C: Init-time schema selection
|
||||
|
||||
```bash
|
||||
# During project init, offer schema customization
|
||||
openspec init
|
||||
# ? Select a workflow schema:
|
||||
# > spec-driven (default)
|
||||
# tdd
|
||||
# minimal
|
||||
# custom (copy and edit)
|
||||
```
|
||||
|
||||
### Recommended Approach
|
||||
|
||||
**Option A** provides the most flexibility and follows Unix conventions (subcommands for discrete operations). Key commands in priority order:
|
||||
|
||||
1. `openspec schema list` — see what's available
|
||||
2. `openspec schema which <name>` — debug resolution
|
||||
3. `openspec schema copy <name>` — scaffold customization
|
||||
4. `openspec schema diff <name>` — compare with built-in
|
||||
5. `openspec schema reset <name>` — revert to defaults
|
||||
|
||||
---
|
||||
|
||||
## Implementation Considerations
|
||||
|
||||
### Path Resolution
|
||||
|
||||
The resolver already exists in `src/core/artifact-graph/resolver.ts`:
|
||||
|
||||
```typescript
|
||||
export function getPackageSchemasDir(): string { ... }
|
||||
export function getUserSchemasDir(): string { ... }
|
||||
export function getSchemaDir(name: string): string | null { ... }
|
||||
export function listSchemas(): string[] { ... }
|
||||
```
|
||||
|
||||
New commands would leverage these existing functions.
|
||||
|
||||
### File Operations
|
||||
|
||||
- Copy should preserve file permissions
|
||||
- Copy should not overwrite existing user files without `--force`
|
||||
- Reset should prompt for confirmation
|
||||
|
||||
### Template-Only Overrides
|
||||
|
||||
A future enhancement could support overriding individual templates without copying the entire schema. This would require changes to the resolution logic:
|
||||
|
||||
```
|
||||
Current: schema dir (user) OR schema dir (built-in)
|
||||
Future: schema.yaml from user OR built-in
|
||||
+ each template from user OR built-in (independent fallback)
|
||||
```
|
||||
|
||||
This adds complexity but enables the "I just want to change one template" use case.
|
||||
|
||||
---
|
||||
|
||||
## Related Documents
|
||||
|
||||
- [Schema Workflow Gaps](./schema-workflow-gaps.md) — End-to-end workflow analysis and phased implementation plan
|
||||
|
||||
## Related Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `src/core/artifact-graph/resolver.ts` | Schema resolution logic |
|
||||
| `src/core/artifact-graph/instruction-loader.ts` | Template loading |
|
||||
| `src/core/global-config.ts` | XDG path helpers |
|
||||
| `schemas/spec-driven/` | Default schema and templates |
|
||||
@@ -0,0 +1,380 @@
|
||||
# Schema Workflow: End-to-End Analysis
|
||||
|
||||
This document analyzes the complete user journey for working with schemas in OpenSpec, identifies gaps, and proposes a phased solution.
|
||||
|
||||
---
|
||||
|
||||
## Current State
|
||||
|
||||
### What Exists
|
||||
|
||||
| Component | Status |
|
||||
|-----------|--------|
|
||||
| Schema resolution (XDG) | 2-level: user override → package built-in |
|
||||
| Built-in schemas | `spec-driven`, `tdd` |
|
||||
| Artifact workflow commands | `status`, `next`, `instructions`, `templates` with `--schema` flag |
|
||||
| Change creation | `openspec new change <name>` — no schema binding |
|
||||
|
||||
### What's Missing
|
||||
|
||||
| Component | Status |
|
||||
|-----------|--------|
|
||||
| Schema bound to change | Not stored — must pass `--schema` every time |
|
||||
| Project-local schemas | Not supported — can't version control with repo |
|
||||
| Schema management CLI | None — manual path discovery required |
|
||||
| Project default schema | None — hardcoded to `spec-driven` |
|
||||
|
||||
---
|
||||
|
||||
## User Journey Analysis
|
||||
|
||||
### Scenario 1: Using a Non-Default Schema
|
||||
|
||||
**Goal:** User wants to use TDD workflow for a new feature.
|
||||
|
||||
**Today's experience:**
|
||||
```bash
|
||||
openspec new change add-auth
|
||||
# Creates directory, no schema info stored
|
||||
|
||||
openspec status --change add-auth
|
||||
# Shows spec-driven artifacts (WRONG - user wanted TDD)
|
||||
|
||||
# User realizes mistake...
|
||||
openspec status --change add-auth --schema tdd
|
||||
# Correct, but must remember --schema every time
|
||||
|
||||
# 6 months later...
|
||||
openspec status --change add-auth
|
||||
# Wrong again - nobody remembers this was TDD
|
||||
```
|
||||
|
||||
**Problems:**
|
||||
- Schema is a runtime argument, not persisted
|
||||
- Easy to forget `--schema` and get wrong results
|
||||
- No record of intended schema for future reference
|
||||
|
||||
---
|
||||
|
||||
### Scenario 2: Customizing a Schema
|
||||
|
||||
**Goal:** User wants to add a "research" artifact before "proposal".
|
||||
|
||||
**Today's experience:**
|
||||
```bash
|
||||
# Step 1: Figure out where to put overrides
|
||||
# Must know XDG conventions:
|
||||
# macOS/Linux: ~/.local/share/openspec/schemas/
|
||||
# Windows: %LOCALAPPDATA%\openspec\schemas/
|
||||
|
||||
# Step 2: Create directory structure
|
||||
mkdir -p ~/.local/share/openspec/schemas/my-workflow/templates
|
||||
|
||||
# Step 3: Find the npm package to copy defaults
|
||||
npm list -g openspec --parseable
|
||||
# Output varies by package manager:
|
||||
# npm: /usr/local/lib/node_modules/openspec
|
||||
# pnpm: ~/.local/share/pnpm/global/5/node_modules/openspec
|
||||
# volta: ~/.volta/tools/image/packages/openspec/...
|
||||
# yarn: ~/.config/yarn/global/node_modules/openspec
|
||||
|
||||
# Step 4: Copy files
|
||||
cp -r <package-path>/schemas/spec-driven/* \
|
||||
~/.local/share/openspec/schemas/my-workflow/
|
||||
|
||||
# Step 5: Edit schema.yaml and templates
|
||||
# No way to verify override is active
|
||||
# No way to diff against original
|
||||
```
|
||||
|
||||
**Problems:**
|
||||
- Must know XDG path conventions
|
||||
- Finding npm package path varies by install method
|
||||
- No tooling to scaffold or verify
|
||||
- No diff capability when upgrading openspec
|
||||
|
||||
---
|
||||
|
||||
### Scenario 3: Team Sharing Custom Workflow
|
||||
|
||||
**Goal:** Team wants everyone to use the same custom schema.
|
||||
|
||||
**Today's options:**
|
||||
1. Everyone manually sets up XDG override — error-prone, drift risk
|
||||
2. Document setup in README — still manual, easy to miss
|
||||
3. Publish separate npm package — overkill for most teams
|
||||
4. Check schema into repo — **not supported** (no project-local resolution)
|
||||
|
||||
**Problems:**
|
||||
- No project-local schema resolution
|
||||
- Can't version control custom schemas with the codebase
|
||||
- No single source of truth for team workflow
|
||||
|
||||
---
|
||||
|
||||
## Gap Summary
|
||||
|
||||
| Gap | Impact | Workaround |
|
||||
|-----|--------|------------|
|
||||
| Schema not bound to change | Wrong results, forgotten context | Remember to pass `--schema` |
|
||||
| No project-local schemas | Can't share via repo | Manual XDG setup per machine |
|
||||
| No schema management CLI | Manual path hunting | Know XDG + find npm package |
|
||||
| No project default schema | Must specify every time | Always pass `--schema` |
|
||||
| No init-time schema selection | Missed setup opportunity | Manual config |
|
||||
|
||||
---
|
||||
|
||||
## Proposed Architecture
|
||||
|
||||
### New File Structure
|
||||
|
||||
```
|
||||
openspec/
|
||||
├── config.yaml # Project config (NEW)
|
||||
├── schemas/ # Project-local schemas (NEW)
|
||||
│ └── my-workflow/
|
||||
│ ├── schema.yaml
|
||||
│ └── templates/
|
||||
│ ├── research.md
|
||||
│ ├── proposal.md
|
||||
│ └── ...
|
||||
└── changes/
|
||||
└── add-auth/
|
||||
├── change.yaml # Change metadata (NEW)
|
||||
├── proposal.md
|
||||
└── ...
|
||||
```
|
||||
|
||||
### config.yaml (Project Config)
|
||||
|
||||
```yaml
|
||||
# openspec/config.yaml
|
||||
defaultSchema: spec-driven
|
||||
```
|
||||
|
||||
Sets the project-wide default schema. Used when:
|
||||
- Creating new changes without `--schema`
|
||||
- Running commands on changes without `change.yaml`
|
||||
|
||||
### change.yaml (Change Metadata)
|
||||
|
||||
```yaml
|
||||
# openspec/changes/add-auth/change.yaml
|
||||
schema: tdd
|
||||
created: 2025-01-15T10:30:00Z
|
||||
description: Add user authentication system
|
||||
```
|
||||
|
||||
Binds a specific schema to a change. Created automatically by `openspec new change`.
|
||||
|
||||
### Schema Resolution Order
|
||||
|
||||
```
|
||||
1. ./openspec/schemas/<name>/ # Project-local
|
||||
2. ~/.local/share/openspec/schemas/<name>/ # User global (XDG)
|
||||
3. <npm-package>/schemas/<name>/ # Built-in
|
||||
```
|
||||
|
||||
Project-local takes priority, enabling version-controlled custom schemas.
|
||||
|
||||
### Schema Selection Order (Per Command)
|
||||
|
||||
```
|
||||
1. --schema CLI flag # Explicit override
|
||||
2. change.yaml in change directory # Change-specific binding
|
||||
3. openspec/config.yaml defaultSchema # Project default
|
||||
4. "spec-driven" # Hardcoded fallback
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ideal User Experience
|
||||
|
||||
### Creating a Change
|
||||
|
||||
```bash
|
||||
# Uses project default (from config.yaml, or spec-driven)
|
||||
openspec new change add-auth
|
||||
# Creates openspec/changes/add-auth/change.yaml:
|
||||
# schema: spec-driven
|
||||
# created: 2025-01-15T10:30:00Z
|
||||
|
||||
# Explicit schema for this change
|
||||
openspec new change add-auth --schema tdd
|
||||
# Creates change.yaml with schema: tdd
|
||||
```
|
||||
|
||||
### Working with Changes
|
||||
|
||||
```bash
|
||||
# Auto-reads schema from change.yaml — no --schema needed
|
||||
openspec status --change add-auth
|
||||
# Output: "Change: add-auth (schema: tdd)"
|
||||
|
||||
openspec next --change add-auth
|
||||
# Knows to use TDD artifacts
|
||||
|
||||
# Explicit override still works (with informational message)
|
||||
openspec status --change add-auth --schema spec-driven
|
||||
# "Note: change.yaml specifies 'tdd', using 'spec-driven' per --schema flag"
|
||||
```
|
||||
|
||||
### Customizing Schemas
|
||||
|
||||
```bash
|
||||
# See what's available
|
||||
openspec schema list
|
||||
# Built-in:
|
||||
# spec-driven proposal → specs → design → tasks
|
||||
# tdd spec → tests → implementation → docs
|
||||
# Project: (none)
|
||||
# User: (none)
|
||||
|
||||
# Copy to project for customization
|
||||
openspec schema copy spec-driven my-workflow
|
||||
# Created ./openspec/schemas/my-workflow/
|
||||
# Edit schema.yaml and templates/ to customize
|
||||
|
||||
# Copy to global (user-level override)
|
||||
openspec schema copy spec-driven --global
|
||||
# Created ~/.local/share/openspec/schemas/spec-driven/
|
||||
|
||||
# See where a schema resolves from
|
||||
openspec schema which spec-driven
|
||||
# ./openspec/schemas/spec-driven/ (project)
|
||||
# or: ~/.local/share/openspec/schemas/spec-driven/ (user)
|
||||
# or: /usr/local/lib/node_modules/openspec/schemas/spec-driven/ (built-in)
|
||||
|
||||
# Compare override with built-in
|
||||
openspec schema diff spec-driven
|
||||
# Shows diff between user/project version and package built-in
|
||||
|
||||
# Remove override, revert to built-in
|
||||
openspec schema reset spec-driven
|
||||
# Removes ./openspec/schemas/spec-driven/ (or --global for user dir)
|
||||
```
|
||||
|
||||
### Project Setup
|
||||
|
||||
```bash
|
||||
openspec init
|
||||
# ? Select default workflow schema:
|
||||
# > spec-driven (proposal → specs → design → tasks)
|
||||
# tdd (spec → tests → implementation → docs)
|
||||
# (custom schemas if detected)
|
||||
#
|
||||
# Writes to openspec/config.yaml:
|
||||
# defaultSchema: spec-driven
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 1: Change Metadata (change.yaml)
|
||||
|
||||
**Priority:** High
|
||||
**Solves:** "Forgot --schema", lost context, wrong results
|
||||
|
||||
**Scope:**
|
||||
- Create `change.yaml` when running `openspec new change`
|
||||
- Store `schema`, `created` timestamp
|
||||
- Modify workflow commands to read schema from `change.yaml`
|
||||
- `--schema` flag overrides (with informational message)
|
||||
- Backwards compatible: missing `change.yaml` → use default
|
||||
|
||||
**change.yaml format:**
|
||||
```yaml
|
||||
schema: tdd
|
||||
created: 2025-01-15T10:30:00Z
|
||||
```
|
||||
|
||||
**Migration:**
|
||||
- Existing changes without `change.yaml` continue to work
|
||||
- Default to `spec-driven` (current behavior)
|
||||
- Optional: `openspec migrate` to add `change.yaml` to existing changes
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: Project-Local Schemas
|
||||
|
||||
**Priority:** High
|
||||
**Solves:** Team sharing, version control, no XDG knowledge needed
|
||||
|
||||
**Scope:**
|
||||
- Add `./openspec/schemas/` to resolution order (first priority)
|
||||
- `openspec schema copy <name> [new-name]` creates in project by default
|
||||
- `--global` flag for user-level XDG directory
|
||||
- Teams can commit `openspec/schemas/` to repo
|
||||
|
||||
**Resolution order:**
|
||||
```
|
||||
1. ./openspec/schemas/<name>/ # Project-local (NEW)
|
||||
2. ~/.local/share/openspec/schemas/<name>/ # User global
|
||||
3. <npm-package>/schemas/<name>/ # Built-in
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: Schema Management CLI
|
||||
|
||||
**Priority:** Medium
|
||||
**Solves:** Path discovery, scaffolding, debugging
|
||||
|
||||
**Commands:**
|
||||
```bash
|
||||
openspec schema list # Show available schemas with sources
|
||||
openspec schema which <name> # Show resolution path
|
||||
openspec schema copy <name> [to] # Copy for customization
|
||||
openspec schema diff <name> # Compare with built-in
|
||||
openspec schema reset <name> # Remove override
|
||||
openspec schema validate <name> # Validate schema.yaml structure
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: Project Config + Init Enhancement
|
||||
|
||||
**Priority:** Low
|
||||
**Solves:** Project-wide defaults, streamlined setup
|
||||
|
||||
**Scope:**
|
||||
- Add `openspec/config.yaml` with `defaultSchema` field
|
||||
- `openspec init` prompts for schema selection
|
||||
- Store selection in `config.yaml`
|
||||
- Commands use as fallback when no `change.yaml` exists
|
||||
|
||||
**config.yaml format:**
|
||||
```yaml
|
||||
defaultSchema: spec-driven
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Backwards Compatibility
|
||||
|
||||
| Scenario | Behavior |
|
||||
|----------|----------|
|
||||
| Existing change without `change.yaml` | Uses `--schema` flag or project default or `spec-driven` |
|
||||
| Existing project without `config.yaml` | Falls back to `spec-driven` |
|
||||
| `--schema` flag provided | Overrides `change.yaml` (with info message) |
|
||||
| No project-local schemas dir | Skipped in resolution, checks user/built-in |
|
||||
|
||||
All existing functionality continues to work. New features are additive.
|
||||
|
||||
---
|
||||
|
||||
## Related Documents
|
||||
|
||||
- [Schema Customization](./schema-customization.md) — Details on manual override process and CLI gaps
|
||||
- [Artifact POC](./artifact_poc.md) — Core artifact graph architecture
|
||||
|
||||
## Related Code
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `src/core/artifact-graph/resolver.ts` | Schema resolution logic |
|
||||
| `src/core/artifact-graph/instruction-loader.ts` | Template loading |
|
||||
| `src/core/global-config.ts` | XDG path helpers |
|
||||
| `src/commands/artifact-workflow.ts` | CLI commands |
|
||||
| `src/utils/change-utils.ts` | Change creation utilities |
|
||||
@@ -6,23 +6,131 @@ artifacts:
|
||||
generates: proposal.md
|
||||
description: Initial proposal document outlining the change
|
||||
template: proposal.md
|
||||
instruction: |
|
||||
Create the proposal document that establishes WHY this change is needed.
|
||||
|
||||
Sections:
|
||||
- **Why**: 1-2 sentences on the problem or opportunity. What problem does this solve? Why now?
|
||||
- **What Changes**: Bullet list of changes. Be specific about new capabilities, modifications, or removals. Mark breaking changes with **BREAKING**.
|
||||
- **Impact**: Two parts:
|
||||
- Affected specs: List capabilities that will have spec deltas
|
||||
- Affected code: Key files, APIs, dependencies, or systems
|
||||
|
||||
Keep it concise (1-2 pages). Focus on the "why" not the "how" -
|
||||
implementation details belong in design.md.
|
||||
|
||||
This is the foundation - specs, design, and tasks all build on this.
|
||||
requires: []
|
||||
|
||||
- id: specs
|
||||
generates: "specs/**/*.md"
|
||||
description: Detailed specifications for the change
|
||||
template: spec.md
|
||||
instruction: |
|
||||
Create specification files that define WHAT the system should do.
|
||||
|
||||
Create one spec file per capability/feature area in specs/<name>/spec.md.
|
||||
|
||||
Delta operations (use ## headers):
|
||||
- **ADDED Requirements**: New capabilities
|
||||
- **MODIFIED Requirements**: Changed behavior - MUST include full updated content
|
||||
- **REMOVED Requirements**: Deprecated features - MUST include **Reason** and **Migration**
|
||||
- **RENAMED Requirements**: Name changes only - use FROM:/TO: format
|
||||
|
||||
Format requirements:
|
||||
- Each requirement: `### Requirement: <name>` followed by description
|
||||
- Use SHALL/MUST for normative requirements (avoid should/may)
|
||||
- Each scenario: `#### Scenario: <name>` with WHEN/THEN format
|
||||
- **CRITICAL**: Scenarios MUST use exactly 4 hashtags (`####`). Using 3 hashtags or bullets will fail silently.
|
||||
- Every requirement MUST have at least one scenario.
|
||||
|
||||
MODIFIED requirements workflow:
|
||||
1. Locate the existing requirement in openspec/specs/<capability>/spec.md
|
||||
2. Copy the ENTIRE requirement block (from `### Requirement:` through all scenarios)
|
||||
3. Paste under `## MODIFIED Requirements` and edit to reflect new behavior
|
||||
4. Ensure header text matches exactly (whitespace-insensitive)
|
||||
|
||||
Common pitfall: Using MODIFIED with partial content loses detail at archive time.
|
||||
If adding new concerns without changing existing behavior, use ADDED instead.
|
||||
|
||||
Example:
|
||||
```
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: User can export data
|
||||
The system SHALL allow users to export their data in CSV format.
|
||||
|
||||
#### Scenario: Successful export
|
||||
- **WHEN** user clicks "Export" button
|
||||
- **THEN** system downloads a CSV file with all user data
|
||||
|
||||
## REMOVED Requirements
|
||||
|
||||
### Requirement: Legacy export
|
||||
**Reason**: Replaced by new export system
|
||||
**Migration**: Use new export endpoint at /api/v2/export
|
||||
```
|
||||
|
||||
Specs should be testable - each scenario is a potential test case.
|
||||
requires:
|
||||
- proposal
|
||||
|
||||
- id: design
|
||||
generates: design.md
|
||||
description: Technical design document with implementation details
|
||||
template: design.md
|
||||
instruction: |
|
||||
Create the design document that explains HOW to implement the change.
|
||||
|
||||
When to include design.md (create only if any apply):
|
||||
- Cross-cutting change (multiple services/modules) or new architectural pattern
|
||||
- New external dependency or significant data model changes
|
||||
- Security, performance, or migration complexity
|
||||
- Ambiguity that benefits from technical decisions before coding
|
||||
|
||||
Sections:
|
||||
- **Context**: Background, current state, constraints, stakeholders
|
||||
- **Goals / Non-Goals**: What this design achieves and explicitly excludes
|
||||
- **Decisions**: Key technical choices with rationale (why X over Y?). Include alternatives considered for each decision.
|
||||
- **Risks / Trade-offs**: Known limitations, things that could go wrong. Format: [Risk] → Mitigation
|
||||
- **Migration Plan**: Steps to deploy, rollback strategy (if applicable)
|
||||
- **Open Questions**: Outstanding decisions or unknowns to resolve
|
||||
|
||||
Focus on architecture and approach, not line-by-line implementation.
|
||||
Reference the proposal for motivation and specs for requirements.
|
||||
|
||||
Good design docs explain the "why" behind technical decisions.
|
||||
requires:
|
||||
- proposal
|
||||
|
||||
- id: tasks
|
||||
generates: tasks.md
|
||||
description: Implementation tasks derived from specs and design
|
||||
template: tasks.md
|
||||
instruction: |
|
||||
Create the task list that breaks down the implementation work.
|
||||
|
||||
Guidelines:
|
||||
- Group related tasks under ## numbered headings
|
||||
- Each task is a checkbox: - [ ] X.Y Task description
|
||||
- Tasks should be small enough to complete in one session
|
||||
- Order tasks by dependency (what must be done first?)
|
||||
|
||||
Example:
|
||||
```
|
||||
## 1. Setup
|
||||
|
||||
- [ ] 1.1 Create new module structure
|
||||
- [ ] 1.2 Add dependencies to package.json
|
||||
|
||||
## 2. Core Implementation
|
||||
|
||||
- [ ] 2.1 Implement data export function
|
||||
- [ ] 2.2 Add CSV formatting utilities
|
||||
```
|
||||
|
||||
Reference specs for what needs to be built, design for how to build it.
|
||||
Each task should be verifiable - you know when it's done.
|
||||
requires:
|
||||
- specs
|
||||
- design
|
||||
|
||||
@@ -6,22 +6,201 @@ artifacts:
|
||||
generates: spec.md
|
||||
description: Feature specification defining requirements
|
||||
template: spec.md
|
||||
instruction: |
|
||||
Create the feature specification that defines WHAT to build.
|
||||
|
||||
Sections:
|
||||
- **Feature**: Name and high-level description of the feature's purpose and user value
|
||||
- **Requirements**: List of specific requirements. Use SHALL/MUST for normative language.
|
||||
- **Acceptance Criteria**: Testable criteria in WHEN/THEN format
|
||||
|
||||
Format requirements:
|
||||
- Each requirement should be specific and testable
|
||||
- Use `#### Scenario: <name>` with WHEN/THEN format for acceptance criteria
|
||||
- Define edge cases and error scenarios explicitly
|
||||
- Every requirement MUST have at least one scenario
|
||||
|
||||
Example:
|
||||
```
|
||||
## Feature: User Authentication
|
||||
|
||||
Users can securely log into the application.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Password validation
|
||||
The system SHALL validate passwords meet minimum security requirements.
|
||||
|
||||
#### Scenario: Valid password accepted
|
||||
- **WHEN** password has 8+ chars, uppercase, lowercase, and number
|
||||
- **THEN** password is accepted
|
||||
|
||||
#### Scenario: Weak password rejected
|
||||
- **WHEN** password is less than 8 characters
|
||||
- **THEN** system displays "Password too short" error
|
||||
```
|
||||
|
||||
This spec drives test creation - each scenario becomes a test case.
|
||||
requires: []
|
||||
|
||||
- id: tests
|
||||
generates: "tests/*.test.ts"
|
||||
description: Test files written before implementation
|
||||
template: test.md
|
||||
instruction: |
|
||||
Write tests BEFORE implementation (TDD red phase).
|
||||
|
||||
File naming:
|
||||
- Create test files as `tests/<feature>.test.ts`
|
||||
- One test file per feature/capability
|
||||
- Use descriptive names matching the spec
|
||||
|
||||
Test structure:
|
||||
- Use Given/When/Then format matching spec scenarios
|
||||
- Group related tests with `describe()` blocks
|
||||
- Each scenario from spec becomes at least one `it()` test
|
||||
|
||||
Coverage requirements:
|
||||
- Cover each requirement from the spec
|
||||
- Include happy path (success cases)
|
||||
- Include edge cases (boundary conditions)
|
||||
- Include error scenarios (invalid input, failures)
|
||||
- Tests should fail initially (no implementation yet)
|
||||
|
||||
Example:
|
||||
```typescript
|
||||
describe('Password validation', () => {
|
||||
it('accepts valid password with all requirements', () => {
|
||||
// GIVEN a password meeting all requirements
|
||||
const password = 'SecurePass1';
|
||||
// WHEN validating
|
||||
const result = validatePassword(password);
|
||||
// THEN it should be accepted
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects password shorter than 8 characters', () => {
|
||||
// GIVEN a short password
|
||||
const password = 'Short1';
|
||||
// WHEN validating
|
||||
const result = validatePassword(password);
|
||||
// THEN it should be rejected with message
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.error).toBe('Password too short');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Follow the spec requirements exactly - tests verify the spec.
|
||||
requires:
|
||||
- spec
|
||||
|
||||
- id: implementation
|
||||
generates: "src/*.ts"
|
||||
description: Implementation code to pass the tests
|
||||
template: implementation.md
|
||||
instruction: |
|
||||
Implement the feature to make tests pass (TDD green phase).
|
||||
|
||||
TDD workflow:
|
||||
1. Run tests - confirm they fail (red)
|
||||
2. Write minimal code to pass ONE test
|
||||
3. Run tests - confirm that test passes (green)
|
||||
4. Refactor if needed while keeping tests green
|
||||
5. Repeat for next failing test
|
||||
|
||||
Implementation guidelines:
|
||||
- Write minimal code to pass each test - no more, no less
|
||||
- Run tests frequently to verify progress
|
||||
- Keep functions small and focused
|
||||
- Use clear, descriptive names
|
||||
|
||||
Code organization:
|
||||
- Create source files in `src/<feature>.ts`
|
||||
- Export public API clearly
|
||||
- Keep implementation details private
|
||||
- Add JSDoc comments for public functions
|
||||
|
||||
Example structure:
|
||||
```typescript
|
||||
/**
|
||||
* Validates a password meets security requirements.
|
||||
* @param password - The password to validate
|
||||
* @returns Validation result with valid flag and optional error
|
||||
*/
|
||||
export function validatePassword(password: string): ValidationResult {
|
||||
if (password.length < 8) {
|
||||
return { valid: false, error: 'Password too short' };
|
||||
}
|
||||
// ... additional checks
|
||||
return { valid: true };
|
||||
}
|
||||
```
|
||||
|
||||
Don't over-engineer - implement only what tests require.
|
||||
requires:
|
||||
- tests
|
||||
|
||||
- id: docs
|
||||
generates: "docs/*.md"
|
||||
description: Documentation for the implemented feature
|
||||
template: docs.md
|
||||
instruction: |
|
||||
Document the implemented feature.
|
||||
|
||||
Sections:
|
||||
- **Overview**: What the feature does and why it exists (1-2 paragraphs)
|
||||
- **Getting Started**: Quick start guide to use the feature immediately
|
||||
- **Examples**: Code examples showing common use cases
|
||||
- **Reference**: Detailed API documentation, configuration options
|
||||
|
||||
Guidelines:
|
||||
- Write for the user, not the developer
|
||||
- Start with the most common use case
|
||||
- Include copy-pasteable code examples
|
||||
- Document all configuration options with defaults
|
||||
- Note any limitations, edge cases, or gotchas
|
||||
- Link to related features or specs
|
||||
|
||||
Example structure:
|
||||
```markdown
|
||||
## Overview
|
||||
|
||||
Password validation ensures user passwords meet security requirements
|
||||
before account creation or password changes.
|
||||
|
||||
## Getting Started
|
||||
|
||||
Import and use the validation function:
|
||||
|
||||
```typescript
|
||||
import { validatePassword } from './password';
|
||||
|
||||
const result = validatePassword('MySecurePass1');
|
||||
if (!result.valid) {
|
||||
console.error(result.error);
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic validation
|
||||
...
|
||||
|
||||
### Custom error handling
|
||||
...
|
||||
|
||||
## Reference
|
||||
|
||||
### validatePassword(password)
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| password | string | The password to validate |
|
||||
|
||||
**Returns**: `{ valid: boolean, error?: string }`
|
||||
```
|
||||
|
||||
Reference the spec for requirements, implementation for details.
|
||||
requires:
|
||||
- implementation
|
||||
|
||||
@@ -312,38 +312,93 @@ async function instructionsCommand(
|
||||
}
|
||||
|
||||
function printInstructionsText(instructions: ArtifactInstructions, isBlocked: boolean): void {
|
||||
const {
|
||||
artifactId,
|
||||
changeName,
|
||||
schemaName,
|
||||
changeDir,
|
||||
outputPath,
|
||||
description,
|
||||
instruction,
|
||||
template,
|
||||
dependencies,
|
||||
unlocks,
|
||||
} = instructions;
|
||||
|
||||
// Opening tag
|
||||
console.log(`<artifact id="${artifactId}" change="${changeName}" schema="${schemaName}">`);
|
||||
console.log();
|
||||
|
||||
// Warning for blocked artifacts
|
||||
if (isBlocked) {
|
||||
console.log(chalk.yellow('Warning: This artifact has unmet dependencies.'));
|
||||
const missing = dependencies.filter((d) => !d.done).map((d) => d.id);
|
||||
console.log('<warning>');
|
||||
console.log('This artifact has unmet dependencies. Complete them first or proceed with caution.');
|
||||
console.log(`Missing: ${missing.join(', ')}`);
|
||||
console.log('</warning>');
|
||||
console.log();
|
||||
}
|
||||
|
||||
console.log(`Artifact: ${instructions.artifactId}`);
|
||||
console.log(`Output: ${instructions.outputPath}`);
|
||||
console.log(`Description: ${instructions.description}`);
|
||||
// Task directive
|
||||
console.log('<task>');
|
||||
console.log(`Create the ${artifactId} artifact for change "${changeName}".`);
|
||||
console.log(description);
|
||||
console.log('</task>');
|
||||
console.log();
|
||||
|
||||
console.log('Dependencies:');
|
||||
if (instructions.dependencies.length === 0) {
|
||||
console.log(' (none)');
|
||||
} else {
|
||||
for (const dep of instructions.dependencies) {
|
||||
const status = dep.done ? chalk.green('[done]') : chalk.red('[missing]');
|
||||
console.log(` ${status} ${dep.id}`);
|
||||
}
|
||||
}
|
||||
console.log();
|
||||
|
||||
if (instructions.unlocks.length > 0) {
|
||||
console.log('Unlocks:');
|
||||
for (const unlocked of instructions.unlocks) {
|
||||
console.log(` ${unlocked}`);
|
||||
// Context (dependencies)
|
||||
if (dependencies.length > 0) {
|
||||
console.log('<context>');
|
||||
console.log('Read these files for context before creating this artifact:');
|
||||
console.log();
|
||||
for (const dep of dependencies) {
|
||||
const status = dep.done ? 'done' : 'missing';
|
||||
const fullPath = path.join(changeDir, dep.path);
|
||||
console.log(`<dependency id="${dep.id}" status="${status}">`);
|
||||
console.log(` <path>${fullPath}</path>`);
|
||||
console.log(` <description>${dep.description}</description>`);
|
||||
console.log('</dependency>');
|
||||
}
|
||||
console.log('</context>');
|
||||
console.log();
|
||||
}
|
||||
|
||||
console.log('Template:');
|
||||
console.log('─'.repeat(40));
|
||||
console.log(instructions.template);
|
||||
// Output location
|
||||
console.log('<output>');
|
||||
console.log(`Write to: ${path.join(changeDir, outputPath)}`);
|
||||
console.log('</output>');
|
||||
console.log();
|
||||
|
||||
// Instruction (guidance)
|
||||
if (instruction) {
|
||||
console.log('<instruction>');
|
||||
console.log(instruction.trim());
|
||||
console.log('</instruction>');
|
||||
console.log();
|
||||
}
|
||||
|
||||
// Template
|
||||
console.log('<template>');
|
||||
console.log(template.trim());
|
||||
console.log('</template>');
|
||||
console.log();
|
||||
|
||||
// Success criteria placeholder
|
||||
console.log('<success_criteria>');
|
||||
console.log('<!-- To be defined in schema validation rules -->');
|
||||
console.log('</success_criteria>');
|
||||
console.log();
|
||||
|
||||
// Unlocks
|
||||
if (unlocks.length > 0) {
|
||||
console.log('<unlocks>');
|
||||
console.log(`Completing this artifact enables: ${unlocks.join(', ')}`);
|
||||
console.log('</unlocks>');
|
||||
console.log();
|
||||
}
|
||||
|
||||
// Closing tag
|
||||
console.log('</artifact>');
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
@@ -36,7 +36,7 @@ export {
|
||||
TemplateLoadError,
|
||||
type ChangeContext,
|
||||
type ArtifactInstructions,
|
||||
type DependencyStatus,
|
||||
type DependencyInfo,
|
||||
type ArtifactStatus,
|
||||
type ChangeStatus,
|
||||
} from './instruction-loader.js';
|
||||
|
||||
@@ -44,26 +44,34 @@ export interface ArtifactInstructions {
|
||||
artifactId: string;
|
||||
/** Schema name */
|
||||
schemaName: string;
|
||||
/** Full path to change directory */
|
||||
changeDir: string;
|
||||
/** Output path pattern (e.g., "proposal.md") */
|
||||
outputPath: string;
|
||||
/** Artifact description */
|
||||
description: string;
|
||||
/** Template content */
|
||||
/** Guidance on how to create this artifact (from schema instruction field) */
|
||||
instruction: string | undefined;
|
||||
/** Template content (structure to follow) */
|
||||
template: string;
|
||||
/** Dependencies with completion status */
|
||||
dependencies: DependencyStatus[];
|
||||
/** Dependencies with completion status and paths */
|
||||
dependencies: DependencyInfo[];
|
||||
/** Artifacts that become available after completing this one */
|
||||
unlocks: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Dependency status information.
|
||||
* Dependency information including path and description.
|
||||
*/
|
||||
export interface DependencyStatus {
|
||||
export interface DependencyInfo {
|
||||
/** Artifact ID */
|
||||
id: string;
|
||||
/** Whether the dependency is completed */
|
||||
done: boolean;
|
||||
/** Relative output path of the dependency (e.g., "proposal.md") */
|
||||
path: string;
|
||||
/** Description of the dependency artifact */
|
||||
description: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -176,15 +184,17 @@ export function generateInstructions(
|
||||
}
|
||||
|
||||
const template = loadTemplate(context.schemaName, artifact.template);
|
||||
const dependencies = getDependencyStatus(artifact, context.completed);
|
||||
const dependencies = getDependencyInfo(artifact, context.graph, context.completed);
|
||||
const unlocks = getUnlockedArtifacts(context.graph, artifactId);
|
||||
|
||||
return {
|
||||
changeName: context.changeName,
|
||||
artifactId: artifact.id,
|
||||
schemaName: context.schemaName,
|
||||
changeDir: context.changeDir,
|
||||
outputPath: artifact.generates,
|
||||
description: artifact.description,
|
||||
instruction: artifact.instruction,
|
||||
template,
|
||||
dependencies,
|
||||
unlocks,
|
||||
@@ -192,16 +202,22 @@ export function generateInstructions(
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets dependency status for an artifact.
|
||||
* Gets dependency info including paths and descriptions.
|
||||
*/
|
||||
function getDependencyStatus(
|
||||
function getDependencyInfo(
|
||||
artifact: Artifact,
|
||||
graph: ArtifactGraph,
|
||||
completed: CompletedSet
|
||||
): DependencyStatus[] {
|
||||
return artifact.requires.map(id => ({
|
||||
id,
|
||||
done: completed.has(id),
|
||||
}));
|
||||
): DependencyInfo[] {
|
||||
return artifact.requires.map(id => {
|
||||
const depArtifact = graph.getArtifact(id);
|
||||
return {
|
||||
id,
|
||||
done: completed.has(id),
|
||||
path: depArtifact?.generates ?? id,
|
||||
description: depArtifact?.description ?? '',
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,6 +6,7 @@ export const ArtifactSchema = z.object({
|
||||
generates: z.string().min(1, { error: 'generates field is required' }),
|
||||
description: z.string(),
|
||||
template: z.string().min(1, { error: 'template field is required' }),
|
||||
instruction: z.string().optional(),
|
||||
requires: z.array(z.string()).default([]),
|
||||
});
|
||||
|
||||
|
||||
@@ -259,9 +259,9 @@ describe('artifact-workflow CLI commands', () => {
|
||||
cwd: tempDir,
|
||||
});
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain('Artifact: proposal');
|
||||
expect(result.stdout).toContain('<artifact id="proposal"');
|
||||
expect(result.stdout).toContain('proposal.md');
|
||||
expect(result.stdout).toContain('Template:');
|
||||
expect(result.stdout).toContain('<template>');
|
||||
});
|
||||
|
||||
it('shows instructions for design artifact', async () => {
|
||||
@@ -271,9 +271,9 @@ describe('artifact-workflow CLI commands', () => {
|
||||
cwd: tempDir,
|
||||
});
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain('Artifact: design');
|
||||
expect(result.stdout).toContain('<artifact id="design"');
|
||||
expect(result.stdout).toContain('design.md');
|
||||
expect(result.stdout).toContain('Template:');
|
||||
expect(result.stdout).toContain('<template>');
|
||||
});
|
||||
|
||||
it('shows blocked warning for artifact with unmet dependencies', async () => {
|
||||
@@ -284,8 +284,8 @@ describe('artifact-workflow CLI commands', () => {
|
||||
cwd: tempDir,
|
||||
});
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain('Warning: This artifact has unmet dependencies');
|
||||
expect(result.stdout).toContain('[missing]');
|
||||
expect(result.stdout).toContain('<warning>');
|
||||
expect(result.stdout).toContain('status="missing"');
|
||||
});
|
||||
|
||||
it('outputs JSON for instructions', async () => {
|
||||
|
||||
@@ -115,19 +115,19 @@ Regular text that should be ignored
|
||||
expect(logOutput.some(line => line.includes('no-tasks') && line.includes('No tasks'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should sort changes alphabetically', async () => {
|
||||
it('should sort changes alphabetically when sort=name', async () => {
|
||||
const changesDir = path.join(tempDir, 'openspec', 'changes');
|
||||
await fs.mkdir(path.join(changesDir, 'zebra'), { recursive: true });
|
||||
await fs.mkdir(path.join(changesDir, 'alpha'), { recursive: true });
|
||||
await fs.mkdir(path.join(changesDir, 'middle'), { recursive: true });
|
||||
|
||||
const listCommand = new ListCommand();
|
||||
await listCommand.execute(tempDir);
|
||||
await listCommand.execute(tempDir, 'changes', { sort: 'name' });
|
||||
|
||||
const changeLines = logOutput.filter(line =>
|
||||
const changeLines = logOutput.filter(line =>
|
||||
line.includes('alpha') || line.includes('middle') || line.includes('zebra')
|
||||
);
|
||||
|
||||
|
||||
expect(changeLines[0]).toContain('alpha');
|
||||
expect(changeLines[1]).toContain('middle');
|
||||
expect(changeLines[2]).toContain('zebra');
|
||||
|
||||
Reference in New Issue
Block a user