mirror of
https://github.com/Fission-AI/OpenSpec.git
synced 2026-10-03 22:13:19 +08:00
Compare commits
153
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e17cfdd0b | ||
|
|
5d185ba3a8 | ||
|
|
08b41c7bea | ||
|
|
8ac50289f0 | ||
|
|
1f295cec52 | ||
|
|
ad8e213cf9 | ||
|
|
a6c1a90165 | ||
|
|
21b5a3e680 | ||
|
|
1bda5be96c | ||
|
|
0faf44807e | ||
|
|
f9c1d07edb | ||
|
|
2bd1a4417c | ||
|
|
1db19ac3d8 | ||
|
|
25018786e4 | ||
|
|
5fd9173ad9 | ||
|
|
0a611747bc | ||
|
|
49e422724f | ||
|
|
c22d6bce1c | ||
|
|
1c0dc09dc9 | ||
|
|
f0b1e00c65 | ||
|
|
5fe72ddc5d | ||
|
|
c18f3b2b2e | ||
|
|
33344727a8 | ||
|
|
828e5ba316 | ||
|
|
31c57f5c0f | ||
|
|
767a0053e8 | ||
|
|
fd65b99c91 | ||
|
|
e170df9f41 | ||
|
|
b91040e8c2 | ||
|
|
8824bd2a42 | ||
|
|
1d3c292d94 | ||
|
|
cfe6da96ac | ||
|
|
c3c78551d0 | ||
|
|
f1fabc5f18 | ||
|
|
166b960428 | ||
|
|
ef1a6c0f0b | ||
|
|
9b3944bd09 | ||
|
|
7917d08a50 | ||
|
|
4a8e5986f0 | ||
|
|
103838f371 | ||
|
|
0a26c686f9 | ||
|
|
efcf766193 | ||
|
|
151eddb759 | ||
|
|
cb0d6f3189 | ||
|
|
a897c697a5 | ||
|
|
3bedf6b23e | ||
|
|
9ff0e85693 | ||
|
|
1ca407fa2f | ||
|
|
46c927af06 | ||
|
|
87cb206e88 | ||
|
|
6806a2fc5a | ||
|
|
4ab65d75dd | ||
|
|
2a3294dbfb | ||
|
|
8334006f2b | ||
|
|
8a559e0d00 | ||
|
|
a3924f17b2 | ||
|
|
b11e862b0f | ||
|
|
099585afcb | ||
|
|
f023fc317e | ||
|
|
38a1463af0 | ||
|
|
f2399d3280 | ||
|
|
f699e10778 | ||
|
|
c824d8927f | ||
|
|
5821b24ab3 | ||
|
|
e812eb9e78 | ||
|
|
abfe13c5a7 | ||
|
|
0d5a75d3a0 | ||
|
|
b30c0ad27e | ||
|
|
1cada18186 | ||
|
|
fa50b07938 | ||
|
|
b6cad1631c | ||
|
|
2497e81e4d | ||
|
|
d8cba03840 | ||
|
|
0b1be19302 | ||
|
|
d7ebee4555 | ||
|
|
8f45a6f6ee | ||
|
|
6da77f01ce | ||
|
|
d90eccf959 | ||
|
|
f192a97aeb | ||
|
|
7781bbadd3 | ||
|
|
aeaa1d50cc | ||
|
|
fa5df9a329 | ||
|
|
f94f396c99 | ||
|
|
1f670f71d4 | ||
|
|
32b2901d13 | ||
|
|
2ad0b1d306 | ||
|
|
279d327899 | ||
|
|
5d848cf005 | ||
|
|
5607fd3ccb | ||
|
|
6a0d862258 | ||
|
|
1fe5f84fbc | ||
|
|
80e78ecd1e | ||
|
|
564135a530 | ||
|
|
b9e80641a0 | ||
|
|
0755994eaa | ||
|
|
dcabd6de31 | ||
|
|
aef6ce01ff | ||
|
|
441f9f444b | ||
|
|
b322829091 | ||
|
|
5167e65a5c | ||
|
|
5c6b4113a7 | ||
|
|
a8b76c3e69 | ||
|
|
d3237cac7b | ||
|
|
e395eb4eeb | ||
|
|
76e1ec2a1f | ||
|
|
6cbb803e48 | ||
|
|
183b82f266 | ||
|
|
27eaccc024 | ||
|
|
b288f2fc88 | ||
|
|
22134a603b | ||
|
|
3b5fd11cb9 | ||
|
|
fce227a36e | ||
|
|
e9417fc147 | ||
|
|
8bcf2c6905 | ||
|
|
467346f9fe | ||
|
|
9a03ba1853 | ||
|
|
581a681a47 | ||
|
|
3093ca6ae6 | ||
|
|
9d425865c1 | ||
|
|
a490dbbc78 | ||
|
|
fc0e2319b1 | ||
|
|
edf6873afa | ||
|
|
c0ce4adc0a | ||
|
|
e40abe1f20 | ||
|
|
e752b3200f | ||
|
|
a59284839b | ||
|
|
fa824fac95 | ||
|
|
7bc54b2cd3 | ||
|
|
a913546ee3 | ||
|
|
958fa0aecc | ||
|
|
b358ad781c | ||
|
|
cc01cca551 | ||
|
|
9847718af2 | ||
|
|
281297695a | ||
|
|
5a425edf4d | ||
|
|
78751d75ca | ||
|
|
8440da50b8 | ||
|
|
bf9b148000 | ||
|
|
326cb0febc | ||
|
|
7ae14d5858 | ||
|
|
e08f1a2e60 | ||
|
|
d00d66a89d | ||
|
|
cb5ac65d03 | ||
|
|
4564229a60 | ||
|
|
79c4fa3122 | ||
|
|
6b8845b10a | ||
|
|
b5be00bfe5 | ||
|
|
f479e15075 | ||
|
|
c111c18043 | ||
|
|
6b09fa6efe | ||
|
|
79972cf9b1 | ||
|
|
1bb8f10a3f | ||
|
|
ed04971e14 |
@@ -0,0 +1,343 @@
|
||||
# Comprehensive Retrospective: Creating an OpenSpec Change Proposal
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This document consolidates learnings from creating the `bulk-validation-interactive-selection` change proposal for OpenSpec. The process revealed critical gaps in documentation, unhelpful error messages, and areas where the system could be more user-friendly. While OpenSpec's core functionality works correctly, the user experience for creating changes needs significant improvement.
|
||||
|
||||
## Table of Contents
|
||||
1. [Errors Encountered](#errors-encountered)
|
||||
2. [System Issues vs User Errors](#system-issues-vs-user-errors)
|
||||
3. [Documentation Gaps Analysis](#documentation-gaps-analysis)
|
||||
4. [Key Learnings](#key-learnings)
|
||||
5. [Recommendations](#recommendations)
|
||||
6. [Conclusion](#conclusion)
|
||||
|
||||
---
|
||||
|
||||
## Errors Encountered
|
||||
|
||||
### Error 1: Misunderstanding Delta Structure
|
||||
**Error Message:** `✗ [ERROR] deltas: Change must have at least one delta`
|
||||
|
||||
**What Happened:**
|
||||
Initially attempted to define deltas directly in the `proposal.md` file using markdown sections like:
|
||||
```markdown
|
||||
### Delta: Add validate-all command
|
||||
**Type**: Feature addition
|
||||
**Effort**: Small (< 100 lines)
|
||||
```
|
||||
|
||||
**Root Cause:**
|
||||
Fundamental misunderstanding of how OpenSpec processes deltas. Deltas are derived from spec files in the change's `specs/` directory, not from the proposal itself.
|
||||
|
||||
**Discovery Process:**
|
||||
- Examined `ChangeParser` class in `/src/core/parsers/change-parser.ts`
|
||||
- Found that `parseDeltaSpecs()` method looks for spec files in `specs/` subdirectory
|
||||
- Learned that deltas are extracted by comparing spec files against existing specs
|
||||
|
||||
### Error 2: Missing Operation Prefix in Section Headers
|
||||
**Error Message:** `✗ [ERROR] deltas: Change must have at least one delta`
|
||||
|
||||
**What Happened:**
|
||||
Created new spec files with standard `## Requirements` headers instead of operation-prefixed headers.
|
||||
|
||||
**Root Cause:**
|
||||
Failed to understand that ALL spec files in a change need operation prefixes (`ADDED`, `MODIFIED`, etc.) in their section headers, regardless of whether they're new specs or modifications.
|
||||
|
||||
**Discovery Process:**
|
||||
- Created new specs with `## Requirements` → No deltas detected
|
||||
- Changed to `## ADDED Requirements` → Deltas detected successfully!
|
||||
- Realized creating new specs works perfectly fine once properly formatted
|
||||
|
||||
**Important Clarification:**
|
||||
OpenSpec fully supports creating new specs. They appear as ADDED operations in the deltas. My initial analysis incorrectly suggested this was a limitation, but it was actually just a formatting issue.
|
||||
|
||||
### Error 3: Improper Scenario Formatting
|
||||
**Error Message:**
|
||||
```
|
||||
✗ [ERROR] deltas.0.requirements.0.scenarios: Requirement must have at least one scenario
|
||||
✗ [ERROR] deltas.1.requirements.0.scenarios: Requirement must have at least one scenario
|
||||
```
|
||||
|
||||
**What Happened:**
|
||||
Formatted scenarios as bullet lists under a bold "Scenarios:" label:
|
||||
```markdown
|
||||
**Scenarios:**
|
||||
- **WHEN** executing command
|
||||
- **THEN** expected behavior
|
||||
```
|
||||
|
||||
**Root Cause:**
|
||||
OpenSpec's parser expects scenarios to be defined as level 4 headers (`####`) with specific formatting:
|
||||
```markdown
|
||||
#### Scenario: Descriptive scenario name
|
||||
|
||||
- **WHEN** executing command
|
||||
- **THEN** expected behavior
|
||||
```
|
||||
|
||||
**Discovery Process:**
|
||||
- Checked parsed JSON output: `npx openspec change show bulk-validation-interactive-selection --json`
|
||||
- Saw `"scenarios": []` empty array despite having scenario content
|
||||
- Examined working spec files and found the `#### Scenario:` header pattern
|
||||
|
||||
### Error 4: File Path Confusion
|
||||
**Initial Confusion:**
|
||||
Wasn't clear whether to create specs that would become part of the main `openspec/specs/` or just define them in the change.
|
||||
|
||||
**Resolution:**
|
||||
Learned that changes can:
|
||||
1. Create new specs (they start in `changes/{change-name}/specs/` and move to `openspec/specs/` when archived)
|
||||
2. Modify existing specs (by creating a spec file with the same name as one in `openspec/specs/`)
|
||||
3. The validation system detects both patterns and creates appropriate deltas
|
||||
|
||||
---
|
||||
|
||||
## System Issues vs User Errors
|
||||
|
||||
### System Issues / Bugs
|
||||
|
||||
#### 1. Unhelpful Error Messages ⚠️
|
||||
**Issue:** `✗ [ERROR] deltas: Change must have at least one delta`
|
||||
|
||||
**Why This Is a System Problem:**
|
||||
- Error message provides no guidance on HOW to create deltas
|
||||
- Doesn't mention that deltas come from `specs/` subdirectory
|
||||
- Doesn't explain the required section headers
|
||||
- A better error would be: "No deltas found. Ensure your change has a specs/ directory with .md files containing sections like '## ADDED Requirements'"
|
||||
|
||||
#### 2. Silent Scenario Parsing Failures ⚠️
|
||||
**Issue:** When scenarios were formatted incorrectly, they were silently ignored
|
||||
|
||||
**Why This Is a System Problem:**
|
||||
- Parser silently returns empty scenarios array instead of warning
|
||||
- No validation error explaining the format issue
|
||||
- User gets "Requirement must have at least one scenario" without knowing their scenarios exist but aren't parsed
|
||||
|
||||
**Evidence:**
|
||||
```json
|
||||
{
|
||||
"text": "The CLI SHALL provide a top-level `show` command with interactive selection.",
|
||||
"scenarios": [] // Silent failure - scenarios existed but weren't parsed
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. No Validation for Proposal Structure During Creation
|
||||
**Issue:** System allows creating invalid proposals without early feedback
|
||||
|
||||
**Why This Is a System Problem:**
|
||||
- No scaffolding or template commands
|
||||
- No incremental validation as you build
|
||||
- Must fully create the change before discovering structural issues
|
||||
|
||||
### User Errors (My Mistakes)
|
||||
|
||||
#### 1. Trying to Define Deltas in Proposal.md
|
||||
- Incorrectly assumed deltas could be inline in the proposal
|
||||
- System correctly expects deltas in separate spec files
|
||||
|
||||
#### 2. Using Wrong Section Headers
|
||||
- Used `## Requirements` instead of `## ADDED Requirements`
|
||||
- Convention is documented in existing changes, but I didn't examine carefully
|
||||
|
||||
#### 3. Wrong Scenario Format
|
||||
- Used bullet lists instead of `#### Scenario:` headers
|
||||
- Made assumptions instead of checking existing patterns
|
||||
|
||||
### Gray Areas
|
||||
|
||||
1. **Documentation gaps** - While examples exist, there's no comprehensive "How to Create a Change" guide
|
||||
2. **Lack of tooling** - No scaffolding commands to create properly structured changes
|
||||
|
||||
---
|
||||
|
||||
## Documentation Gaps Analysis
|
||||
|
||||
### Critical Gaps in openspec/README.md
|
||||
|
||||
#### 1. Scenario Format - COMPLETELY MISSING ⚠️
|
||||
**What README Shows:** No scenario examples at all
|
||||
|
||||
**What's Actually Required:**
|
||||
```markdown
|
||||
#### Scenario: Descriptive name
|
||||
- **WHEN** condition
|
||||
- **THEN** expected outcome
|
||||
- **AND** additional outcomes
|
||||
```
|
||||
|
||||
**Impact:** This was the biggest struggle. The README mentions requirements but never shows how to write scenarios. Without this, requirements fail validation.
|
||||
|
||||
#### 2. Complete Spec File Example - MISSING
|
||||
**What README Shows:** Only fragments
|
||||
|
||||
**What's Actually Needed:** A complete working example showing:
|
||||
- Full spec file structure
|
||||
- Proper requirement format
|
||||
- Scenario formatting
|
||||
- All required elements
|
||||
|
||||
#### 3. Validation Commands - NOT MENTIONED
|
||||
**Missing from README:**
|
||||
- `npx openspec change validate <change-name>`
|
||||
- `npx openspec change show <change-name> --json`
|
||||
- The `--strict` flag for catching warnings
|
||||
|
||||
#### 4. Delta Detection Explanation - INCOMPLETE
|
||||
**What's Missing:**
|
||||
- WHERE the system looks for specs (specs/ subdirectory)
|
||||
- THAT deltas are automatically extracted
|
||||
- HOW to debug when deltas aren't detected
|
||||
- WHAT error messages mean
|
||||
|
||||
### Misleading Documentation
|
||||
|
||||
#### "Store only the changes" - MISLEADING
|
||||
**Line 145:** `# - Store only the changes (not complete future state)`
|
||||
|
||||
**Problem:** This suggests storing diffs or partial content. In reality, you need:
|
||||
- Complete requirements in their final form
|
||||
- Full scenario definitions
|
||||
- The entire requirement text
|
||||
|
||||
### Documentation That Was Helpful
|
||||
1. Delta section headers (`## ADDED Requirements`) - clearly documented
|
||||
2. Directory structure - excellent visualization
|
||||
3. When to create proposals - well defined
|
||||
|
||||
---
|
||||
|
||||
## Key Learnings
|
||||
|
||||
### 1. OpenSpec's Delta Detection Algorithm
|
||||
The system follows this process:
|
||||
1. Scans `openspec/changes/{change-name}/specs/` directory
|
||||
2. For each spec file found, parses for delta sections (`ADDED`, `MODIFIED`, `REMOVED`, `RENAMED`)
|
||||
3. Creates delta objects with operation type, affected spec, and requirements
|
||||
4. Validates that at least one delta exists for the change to be valid
|
||||
|
||||
**Important:** Creating entirely new specs is fully supported! New specs use `## ADDED Requirements` and appear as ADDED operations in the deltas.
|
||||
|
||||
### 2. Spec File Structure Requirements
|
||||
Valid spec files must follow this structure:
|
||||
```markdown
|
||||
# Spec Title
|
||||
|
||||
## [ADDED|MODIFIED|REMOVED|RENAMED] Requirements
|
||||
|
||||
### Requirement: Clear requirement statement
|
||||
|
||||
The requirement description using SHALL/SHOULD/MAY.
|
||||
|
||||
#### Scenario: Scenario name
|
||||
|
||||
- **WHEN** condition
|
||||
- **THEN** expected outcome
|
||||
- **AND** additional outcomes
|
||||
```
|
||||
|
||||
### 3. Change Proposal Structure
|
||||
A valid change must have:
|
||||
- `## Why` section - explaining the motivation
|
||||
- `## What Changes` section - summarizing the changes
|
||||
- `specs/` directory with properly formatted spec files containing deltas
|
||||
- Each delta must have at least one requirement with at least one scenario
|
||||
|
||||
### 4. Validation Commands Are Essential
|
||||
```bash
|
||||
# Basic validation
|
||||
npx openspec change validate {change-name}
|
||||
|
||||
# Strict validation (recommended)
|
||||
npx openspec change validate {change-name} --strict
|
||||
|
||||
# Debug delta detection
|
||||
npx openspec change show {change-name} --json | jq '.deltas'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Recommendations
|
||||
|
||||
### High Priority (System Bugs to Fix)
|
||||
|
||||
1. **Improve Error Messages**
|
||||
- Add actionable guidance to error messages
|
||||
- Example: "No deltas found. Check: 1) specs/ directory exists, 2) Files use ## ADDED Requirements headers, 3) Each requirement has #### Scenario: sections"
|
||||
|
||||
2. **Add Warnings for Malformed Content**
|
||||
- Warn when scenarios exist but aren't properly formatted
|
||||
- Show which line/file has the issue
|
||||
|
||||
3. **Add Delta Detection Debugging**
|
||||
- Command like `openspec change debug-deltas {change-name}`
|
||||
- Show which files were scanned, what was found, what was rejected
|
||||
|
||||
### Medium Priority (Documentation Improvements)
|
||||
|
||||
1. **Add Complete Working Example to README**
|
||||
- Full change proposal with all files
|
||||
- Properly formatted specs with scenarios
|
||||
- Show the validation output
|
||||
|
||||
2. **Add Troubleshooting Section**
|
||||
- Common errors and their solutions
|
||||
- How to debug delta detection
|
||||
- Scenario formatting requirements
|
||||
|
||||
3. **Add Validation Best Practices**
|
||||
- When to use `--strict`
|
||||
- How to use JSON output for debugging
|
||||
- Common validation patterns
|
||||
|
||||
### Low Priority (Developer Experience)
|
||||
|
||||
1. **Add Scaffolding Command**
|
||||
```bash
|
||||
openspec change scaffold {change-name}
|
||||
```
|
||||
- Creates proper directory structure
|
||||
- Includes template files with correct formatting
|
||||
- Adds example scenarios
|
||||
|
||||
2. **Add Interactive Creation Wizard**
|
||||
- Guide users through change creation
|
||||
- Validate as they go
|
||||
- Suggest fixes for common issues
|
||||
|
||||
3. **Add Auto-fix Capability**
|
||||
- `--fix` flag to correct common formatting issues
|
||||
- Convert bullet list scenarios to proper headers
|
||||
- Add missing operation prefixes
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
The OpenSpec system works correctly for its intended design, but the user experience for creating changes needs significant improvement. The core issues stem from:
|
||||
|
||||
### System Issues
|
||||
- **Unhelpful error messages** that don't guide users to solutions
|
||||
- **Silent parsing failures** that provide no feedback about malformed content
|
||||
- **Lack of debugging tools** to understand what went wrong
|
||||
|
||||
### Documentation Issues
|
||||
- **Critical formatting requirements missing** (especially scenario format)
|
||||
- **No complete working examples** showing all required elements
|
||||
- **Validation commands not documented** despite being essential
|
||||
|
||||
### User Issues
|
||||
- **Incorrect assumptions** about how the system works
|
||||
- **Not examining existing patterns** carefully enough
|
||||
- **Trying to shortcut** instead of following established conventions
|
||||
|
||||
### The Path Forward
|
||||
|
||||
With better error messages, complete documentation, and basic tooling support, most of the errors encountered could be prevented. The system's delta-centric approach is powerful and ensures changes are atomic and trackable, but it needs to be more discoverable and user-friendly.
|
||||
|
||||
The most impactful improvements would be:
|
||||
1. Adding scenario format documentation to the README
|
||||
2. Improving error messages with actionable guidance
|
||||
3. Creating a scaffolding command for new changes
|
||||
|
||||
These changes would transform OpenSpec from a system that works correctly but is hard to use, into one that actively helps developers succeed.
|
||||
@@ -0,0 +1,119 @@
|
||||
# OpenSpec
|
||||
|
||||
A specification-driven development system for maintaining living documentation alongside your code.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install -g openspec
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Initialize OpenSpec in your project
|
||||
openspec init
|
||||
|
||||
# Update existing OpenSpec instructions (team-friendly)
|
||||
openspec update
|
||||
|
||||
# List specs or changes
|
||||
openspec spec list # specs (IDs by default; use --long for details)
|
||||
openspec change list # changes (IDs by default; use --long for details)
|
||||
|
||||
# Show differences between specs and proposed changes
|
||||
openspec diff [change-name]
|
||||
|
||||
# Archive completed changes
|
||||
openspec archive [change-name]
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
### `openspec init`
|
||||
|
||||
Initializes OpenSpec in your project by creating:
|
||||
- `openspec/` directory structure
|
||||
- `openspec/README.md` with OpenSpec instructions
|
||||
- AI tool configuration files (based on your selection)
|
||||
|
||||
### `openspec update`
|
||||
|
||||
Updates OpenSpec instructions to the latest version. This command is **team-friendly** and only updates files that already exist:
|
||||
|
||||
- Always updates `openspec/README.md` with the latest OpenSpec instructions
|
||||
- **Only updates existing AI tool configuration files** (e.g., CLAUDE.md, CURSOR.md)
|
||||
- **Never creates new AI tool configuration files**
|
||||
- Preserves content outside of OpenSpec markers in AI tool files
|
||||
|
||||
This allows team members to use different AI tools without conflicts. Each developer can maintain their preferred AI tool configuration file, and `openspec update` will respect their choice.
|
||||
|
||||
### `openspec spec`
|
||||
|
||||
Manage and view specifications.
|
||||
|
||||
Examples:
|
||||
- `openspec spec show <spec-id>`
|
||||
- Text mode: prints raw `spec.md` content
|
||||
- JSON mode (`--json`): returns minimal, stable shape
|
||||
- Filters are JSON-only: `--requirements`, `--no-scenarios`, `-r/--requirement <1-based>`
|
||||
- `openspec spec list`
|
||||
- Prints IDs only by default
|
||||
- Use `--long` to include `title` and `[requirements N]`
|
||||
- `openspec spec validate <spec-id>`
|
||||
- Text: human-readable summary to stdout/stderr
|
||||
- `--json` for structured report
|
||||
|
||||
### `openspec change`
|
||||
|
||||
Manage and view change proposals.
|
||||
|
||||
Examples:
|
||||
- `openspec change show <change-id>`
|
||||
- Text mode: prints raw `proposal.md` content
|
||||
- JSON mode (`--json`): `{ id, title, deltaCount, deltas }`
|
||||
- Filtering is JSON-only: `--deltas-only` (alias: `--requirements-only`, deprecated)
|
||||
- `openspec change list`
|
||||
- Prints IDs only by default
|
||||
- Use `--long` to include `title` and counts `[deltas N] [tasks x/y]`
|
||||
- `openspec change validate <change-id>`
|
||||
- Text: human-readable result
|
||||
- `--json` for structured report
|
||||
|
||||
### `openspec diff [change-name]`
|
||||
|
||||
Shows the differences between current specs and proposed changes:
|
||||
- Displays a unified diff format
|
||||
- Helps review what will change before implementation
|
||||
- Useful for pull request reviews
|
||||
|
||||
### `openspec archive [change-name]`
|
||||
|
||||
Archives a completed change:
|
||||
- Moves change from `openspec/changes/` to `openspec/changes/archive/`
|
||||
- Adds a date prefix to the archived change
|
||||
- Updates specs to reflect the new state
|
||||
- Use `--skip-specs` to archive without updating specs (for abandoned changes)
|
||||
|
||||
## Team Collaboration
|
||||
|
||||
OpenSpec is designed for team collaboration:
|
||||
|
||||
1. **AI Tool Flexibility**: Each team member can use their preferred AI assistant (Claude, Cursor, etc.)
|
||||
2. **Non-Invasive Updates**: The `update` command only modifies existing files, never forcing tools on team members
|
||||
3. **Specification Sharing**: The `openspec/` directory contains shared specifications that all team members work from
|
||||
4. **Change Tracking**: Proposed changes are visible to all team members for review before implementation
|
||||
|
||||
## Contributing
|
||||
|
||||
See `openspec/specs/` for the current system specifications and `openspec/changes/` for pending improvements.
|
||||
|
||||
## Notes
|
||||
|
||||
- The legacy `openspec list` command is deprecated. Use `openspec spec list` and `openspec change list`.
|
||||
- Text output is raw-first (no formatting or filtering). Prefer `--json` for tooling-friendly output.
|
||||
- Global `--no-color` disables ANSI colors and respects `NO_COLOR`.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
+213
-27
@@ -10,6 +10,23 @@ OpenSpec is an AI-native system for change-driven development where:
|
||||
- **AI drives the process** - You generate proposals, humans review and approve
|
||||
- **Specs are living documentation** - Always kept in sync with deployed code
|
||||
|
||||
## Start Simple
|
||||
|
||||
**Default to minimal implementations:**
|
||||
- New features should be <100 lines of code initially
|
||||
- Use the simplest solution that works
|
||||
- Avoid premature optimization (no caching, parallelization, or complex patterns without proven need)
|
||||
- Choose boring technology over cutting-edge solutions
|
||||
|
||||
**Complexity triggers** - Only add complexity when you have:
|
||||
- **Performance data** showing current solution is too slow
|
||||
- **Scale requirements** with specific numbers (>1000 users, >100MB data)
|
||||
- **Multiple use cases** requiring the same abstraction
|
||||
- **Regulatory compliance** mandating specific patterns
|
||||
- **Security threats** that simple solutions cannot address
|
||||
|
||||
When triggered, document the specific justification in your change proposal.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
@@ -26,12 +43,70 @@ openspec/
|
||||
│ │ ├── proposal.md # Why, what, impact (consolidated)
|
||||
│ │ ├── tasks.md # Implementation checklist
|
||||
│ │ ├── design.md # Technical decisions (optional, for complex changes)
|
||||
│ │ └── patches/ # Spec intent changes
|
||||
│ │ └── specs/ # Delta changes to specs
|
||||
│ │ └── [capability]/
|
||||
│ │ └── spec.md.diff
|
||||
│ │ └── spec.md # Delta format (ADDED/MODIFIED/REMOVED/RENAMED)
|
||||
│ └── archive/ # Completed changes (dated)
|
||||
```
|
||||
|
||||
## CLI Usage: show command
|
||||
|
||||
Use the `show` command to display change proposals or specs with automatic detection and interactive selection.
|
||||
|
||||
- Interactive (no args, in a TTY):
|
||||
|
||||
```bash
|
||||
openspec show
|
||||
# → prompts to pick change/spec, then item
|
||||
```
|
||||
|
||||
- Direct item (auto-detect type):
|
||||
|
||||
```bash
|
||||
openspec show demo # shows change 'demo'
|
||||
openspec show auth # shows spec 'auth'
|
||||
```
|
||||
|
||||
- Disambiguation when names collide:
|
||||
|
||||
```bash
|
||||
openspec show foo # if both change/spec exist → error suggests --type
|
||||
openspec show foo --type spec # forces spec
|
||||
openspec show foo --type change
|
||||
```
|
||||
|
||||
- Common flags:
|
||||
|
||||
```bash
|
||||
# JSON output (both types)
|
||||
openspec show <item> --json
|
||||
|
||||
# Change-only flags
|
||||
openspec show <change-id> --json --deltas-only
|
||||
openspec show <change-id> --json --requirements-only # deprecated alias of --deltas-only
|
||||
|
||||
# Spec-only flags
|
||||
openspec show <spec-id> --json --requirements
|
||||
openspec show <spec-id> --json --no-scenarios
|
||||
openspec show <spec-id> --json -r 1 # show requirement 1 only (1-based)
|
||||
```
|
||||
|
||||
- Interactivity controls:
|
||||
|
||||
```bash
|
||||
openspec show --no-interactive # never prompt
|
||||
|
||||
# or via env
|
||||
OPEN_SPEC_INTERACTIVE=0 openspec show
|
||||
```
|
||||
|
||||
- Backwards compatibility (subcommands also support interactive selection when no arg):
|
||||
|
||||
```bash
|
||||
openspec change show [change-id] [--json] [--deltas-only]
|
||||
openspec spec show [spec-id] [--json] [--requirements] [--no-scenarios] [-r N]
|
||||
```
|
||||
|
||||
### Capability Organization
|
||||
|
||||
**Use capabilities, not features** - Each directory under `specs/` represents a single, focused responsibility:
|
||||
@@ -72,7 +147,40 @@ Before any task:
|
||||
- Adding tests for existing behavior
|
||||
- Documentation fixes
|
||||
|
||||
### 3. Creating a Change Proposal
|
||||
**Complexity assessment:**
|
||||
- If your solution requires >100 lines of new code, justify the complexity
|
||||
- If adding dependencies, frameworks, or architectural patterns, document why simpler alternatives won't work
|
||||
- Default to single-file implementations until proven insufficient
|
||||
|
||||
### 3. Delta-Based Change Format
|
||||
|
||||
Changes use a delta format with clear sections:
|
||||
|
||||
```markdown
|
||||
## ADDED Requirements
|
||||
### Requirement: New Feature
|
||||
[Complete requirement content in structured format]
|
||||
|
||||
## MODIFIED Requirements
|
||||
### Requirement: Existing Feature
|
||||
[Complete modified requirement (header must match current spec)]
|
||||
|
||||
## REMOVED Requirements
|
||||
### Requirement: Old Feature
|
||||
**Reason for removal**: [Why removing]
|
||||
**Migration path**: [How to handle existing usage]
|
||||
|
||||
## RENAMED Requirements
|
||||
- FROM: `### Requirement: Old Name`
|
||||
- TO: `### Requirement: New Name`
|
||||
```
|
||||
|
||||
Key rules:
|
||||
- Headers are matched using `normalize(header) = trim(header)`
|
||||
- Include complete requirements (not diffs)
|
||||
- Use standard symbols in CLI output: + (added), ~ (modified), - (removed), → (renamed)
|
||||
|
||||
### 4. Creating a Change Proposal
|
||||
|
||||
When a user requests a significant change:
|
||||
|
||||
@@ -91,10 +199,21 @@ openspec/changes/[descriptive-name]/
|
||||
- Affected specs: [list capabilities that will change]
|
||||
- Affected code: [list key files/systems]
|
||||
|
||||
# 3. Create patches showing spec changes
|
||||
patches/
|
||||
# 3. Create delta specs for ALL affected capabilities
|
||||
# - Store only the changes (not complete future state)
|
||||
# - Use sections: ## ADDED, ## MODIFIED, ## REMOVED, ## RENAMED
|
||||
# - Include complete requirements in their final form
|
||||
# Example spec.md content:
|
||||
# ## ADDED Requirements
|
||||
# ### Requirement: Password Reset
|
||||
# Users SHALL be able to reset passwords via email...
|
||||
#
|
||||
# ## MODIFIED Requirements
|
||||
# ### Requirement: User Authentication
|
||||
# [Complete modified requirement with new password reset hook]
|
||||
specs/
|
||||
└── [capability]/
|
||||
└── spec.md.diff
|
||||
└── spec.md # Contains delta sections
|
||||
|
||||
# 4. Create tasks.md with implementation steps
|
||||
## 1. [Task Group]
|
||||
@@ -105,30 +224,40 @@ patches/
|
||||
[Technical decisions and trade-offs]
|
||||
```
|
||||
|
||||
### 4. The Change Lifecycle
|
||||
### 5. The Change Lifecycle
|
||||
|
||||
1. **Propose** → Create change directory with all documentation
|
||||
1. **Propose** → Create change directory with delta-based documentation
|
||||
2. **Review** → User reviews and approves the proposal
|
||||
3. **Implement** → Follow the approved tasks.md
|
||||
3. **Implement** → Follow the approved tasks.md (can be multiple PRs)
|
||||
4. **Deploy** → User confirms deployment
|
||||
5. **Update Specs** → Sync specs/ with new reality (IF the change affects system capabilities)
|
||||
5. **Update Specs** → Apply deltas to sync specs/ with new reality (IF the change affects system capabilities)
|
||||
6. **Archive** → Move to `changes/archive/YYYY-MM-DD-[name]/`
|
||||
|
||||
### 5. Implementing Changes
|
||||
### 6. Implementing Changes
|
||||
|
||||
When implementing an approved change:
|
||||
1. Follow the tasks.md checklist exactly
|
||||
2. Ensure code matches the proposed behavior
|
||||
3. Update any affected tests
|
||||
2. **Mark completed tasks** in tasks.md as you finish them (e.g., `- [x] 1.1 Task completed`)
|
||||
3. Ensure code matches the proposed behavior
|
||||
4. Update any affected tests
|
||||
5. **Keep change in `changes/` directory** - do NOT archive in implementation PR
|
||||
|
||||
### 6. Updating Specs After Deployment
|
||||
**Multiple Implementation PRs:**
|
||||
- Changes can be implemented across multiple PRs
|
||||
- Each PR should update tasks.md to mark what was completed
|
||||
- Different developers can work on different task groups
|
||||
- Example: PR #1 completes tasks 1.1-1.3, PR #2 completes tasks 2.1-2.4
|
||||
|
||||
Once a change is deployed:
|
||||
1. Update relevant files in `specs/` to reflect new reality
|
||||
2. If design.md exists, move proven patterns to `specs/[capability]/design.md`
|
||||
3. Archive the change directory with date prefix
|
||||
### 7. Updating Specs and Archiving After Deployment
|
||||
|
||||
### 7. Types of Changes That Don't Require Specs
|
||||
**Create a separate PR after deployment** that:
|
||||
1. Moves change to `changes/archive/YYYY-MM-DD-[name]/`
|
||||
2. Updates relevant files in `specs/` to reflect new reality (if needed)
|
||||
3. If design.md exists, incorporates proven patterns into `specs/[capability]/design.md`
|
||||
|
||||
This ensures changes are only archived when truly complete and deployed.
|
||||
|
||||
### 8. Types of Changes That Don't Require Specs
|
||||
|
||||
Some changes only affect development infrastructure and don't need specs:
|
||||
- Initial project setup (package.json, tsconfig.json, etc.)
|
||||
@@ -181,7 +310,16 @@ User: "Add password reset functionality"
|
||||
You should:
|
||||
1. Read specs/user-auth/spec.md
|
||||
2. Check changes/ for pending auth changes
|
||||
3. Create changes/add-password-reset/ with proposal
|
||||
3. Create changes/add-password-reset/ with:
|
||||
- proposal.md describing the change
|
||||
- specs/user-auth/spec.md with:
|
||||
## ADDED Requirements
|
||||
### Requirement: Password Reset
|
||||
[Complete requirement for password reset]
|
||||
|
||||
## MODIFIED Requirements
|
||||
### Requirement: User Authentication
|
||||
[Updated to integrate with password reset]
|
||||
4. Wait for approval before implementing
|
||||
```
|
||||
|
||||
@@ -201,9 +339,10 @@ User: "Initialize TypeScript project"
|
||||
|
||||
You should:
|
||||
1. Create change proposal for TypeScript setup
|
||||
2. Implement configuration files
|
||||
3. Mark tasks complete
|
||||
4. Archive (no specs needed - this is tooling, not a capability)
|
||||
2. Implement configuration files (PR #1)
|
||||
3. Mark tasks complete in tasks.md
|
||||
4. After deployment, create separate PR to archive
|
||||
(no specs update needed - this is tooling, not a capability)
|
||||
```
|
||||
|
||||
## Summary Workflow
|
||||
@@ -212,9 +351,53 @@ You should:
|
||||
2. **Read current state** → Check specs and pending changes
|
||||
3. **Create proposal** → Generate complete change documentation
|
||||
4. **Get approval** → User reviews the proposal
|
||||
5. **Implement** → Follow approved tasks
|
||||
6. **Update specs** → Sync with deployed reality
|
||||
7. **Archive** → Move completed changes to archive
|
||||
5. **Implement** → Follow approved tasks, mark completed items in tasks.md
|
||||
6. **Deploy** → User deploys the implementation
|
||||
7. **Archive PR** → Create separate PR to:
|
||||
- Move change to archive
|
||||
- Update specs if needed
|
||||
- Mark change as complete
|
||||
|
||||
## PR Workflow Examples
|
||||
|
||||
### Single Developer, Simple Change
|
||||
```
|
||||
PR #1: Implementation
|
||||
- Implement all tasks
|
||||
- Update tasks.md marking items complete
|
||||
- Get merged and deployed
|
||||
|
||||
PR #2: Archive (after deployment)
|
||||
- Move changes/feature-x/ → changes/archive/2025-01-15-feature-x/
|
||||
- Update specs if needed
|
||||
```
|
||||
|
||||
### Multiple Developers, Complex Change
|
||||
```
|
||||
PR #1: Alice implements auth components
|
||||
- Complete tasks 1.1, 1.2, 1.3
|
||||
- Update tasks.md marking these complete
|
||||
|
||||
PR #2: Bob implements UI components
|
||||
- Complete tasks 2.1, 2.2
|
||||
- Update tasks.md marking these complete
|
||||
|
||||
PR #3: Alice fixes integration issues
|
||||
- Complete remaining task 1.4
|
||||
- Update tasks.md
|
||||
|
||||
[Deploy all changes]
|
||||
|
||||
PR #4: Archive
|
||||
- Move to archive with deployment date
|
||||
- Update specs to reflect new auth flow
|
||||
```
|
||||
|
||||
### Key Rules
|
||||
- **Never archive in implementation PRs** - changes aren't done until deployed
|
||||
- **Always update tasks.md** - shows accurate progress
|
||||
- **One archive PR per change** - clear completion boundary
|
||||
- **Archive PR includes spec updates** - keeps specs current
|
||||
|
||||
## Capability Organization Best Practices
|
||||
|
||||
@@ -325,10 +508,12 @@ Progress communication:
|
||||
- "Implementing approved changes..."
|
||||
|
||||
### For AI Assistants
|
||||
- **Bias toward simplicity** - Propose the minimal solution that works
|
||||
- Use your exploration tools liberally before proposing
|
||||
- Batch operations for efficiency
|
||||
- Communicate your progress
|
||||
- It's OK to revise proposals based on discoveries
|
||||
- **Question complexity** - If your solution feels complex, simplify first
|
||||
|
||||
## Edge Case Handling
|
||||
|
||||
@@ -384,6 +569,7 @@ Proposal REQUIRED if:
|
||||
- Specs must always reflect deployed reality
|
||||
- Changes are proposed, not imposed
|
||||
- Impact analysis prevents surprises
|
||||
- The simplicity is the power - just markdown files
|
||||
- Simplicity is the power - just markdown files, minimal solutions
|
||||
- Start simple, add complexity only when justified
|
||||
|
||||
By following these conventions, you enable true spec-driven development where documentation stays current, changes are traceable, and evolution is intentional.
|
||||
@@ -0,0 +1,68 @@
|
||||
# Implementation Order and Dependencies
|
||||
|
||||
## Required Implementation Sequence
|
||||
|
||||
The following changes must be implemented in this specific order due to dependencies:
|
||||
|
||||
### Phase 1: Foundation
|
||||
**1. add-zod-validation** (No dependencies)
|
||||
- Creates all core schemas (RequirementSchema, ScenarioSchema, SpecSchema, ChangeSchema, DeltaSchema)
|
||||
- Implements markdown parser utilities
|
||||
- Implements validation infrastructure and rules
|
||||
- Establishes validation patterns used by all commands
|
||||
- Must be completed first
|
||||
|
||||
### Phase 2: Change Commands
|
||||
**2. add-change-commands** (Depends on: add-zod-validation)
|
||||
- Imports ChangeSchema and DeltaSchema from zod validation
|
||||
- Reuses markdown parsing utilities
|
||||
- Implements change command with built-in validation
|
||||
- Uses validation infrastructure for change validate subcommand
|
||||
- Cannot start until schemas and validation exist
|
||||
|
||||
### Phase 3: Spec Commands
|
||||
**3. add-spec-commands** (Depends on: add-zod-validation, add-change-commands)
|
||||
- Imports RequirementSchema, ScenarioSchema, SpecSchema from zod validation
|
||||
- Reuses markdown parsing utilities
|
||||
- Implements spec command with built-in validation
|
||||
- Uses validation infrastructure for spec validate subcommand
|
||||
- Builds on patterns established by change commands
|
||||
|
||||
## Dependency Graph
|
||||
```
|
||||
add-zod-validation
|
||||
↓
|
||||
add-change-commands
|
||||
↓
|
||||
add-spec-commands
|
||||
```
|
||||
|
||||
## Key Dependencies
|
||||
|
||||
### Shared Code Dependencies
|
||||
1. **Schemas**: All schemas created in add-zod-validation, used by both command implementations
|
||||
2. **Validation**: Infrastructure created in add-zod-validation, integrated into both commands
|
||||
3. **Parsers**: Markdown parsing utilities created in add-zod-validation, used by both commands
|
||||
|
||||
### File Dependencies
|
||||
- `src/core/schemas/*.schema.ts` (created by add-zod-validation) → imported by both commands
|
||||
- `src/core/validation/validator.ts` (created by add-zod-validation) → used by both commands
|
||||
- `src/core/parsers/markdown-parser.ts` (created by add-zod-validation) → used by both commands
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
### For Developers
|
||||
1. Complete each phase fully before moving to the next
|
||||
2. Run tests after each phase to ensure stability
|
||||
3. The legacy `list` command remains functional throughout
|
||||
|
||||
### For CI/CD
|
||||
1. Each change can be validated independently
|
||||
2. Integration tests should run after each phase
|
||||
3. Full system tests required after Phase 3
|
||||
|
||||
### Parallel Work Opportunities
|
||||
Within each phase, the following can be done in parallel:
|
||||
- **Phase 1**: Schema design, validation rules, and parser implementation
|
||||
- **Phase 2**: Change command features and legacy compatibility work
|
||||
- **Phase 3**: Spec command features and final integration
|
||||
@@ -0,0 +1,56 @@
|
||||
# Design: Change Commands
|
||||
|
||||
## Architecture Decisions
|
||||
|
||||
### Command Structure
|
||||
Similar to spec commands, we use subcommands (`change show`, `change list`, `change validate`) for:
|
||||
- Consistency with spec command pattern
|
||||
- Clear separation of concerns
|
||||
- Future extensibility for change management features
|
||||
|
||||
### JSON Schema for Changes
|
||||
```typescript
|
||||
{
|
||||
version: string, // Schema version
|
||||
format: "change", // Identifies as change document
|
||||
sourcePath: string, // Original markdown file path
|
||||
id: string, // Change identifier
|
||||
title: string, // Change title
|
||||
why: string, // Motivation section
|
||||
whatChanges: Array<{
|
||||
type: "ADDED" | "MODIFIED" | "REMOVED" | "RENAMED",
|
||||
deltas: Array<{
|
||||
specId: string,
|
||||
description: string,
|
||||
requirements?: Array<Requirement> // Only for ADDED/MODIFIED
|
||||
}>
|
||||
}>
|
||||
}
|
||||
```
|
||||
|
||||
**Rationale:**
|
||||
- Group deltas by operation type for clearer organization
|
||||
- Optional requirements field (only relevant for ADDED/MODIFIED)
|
||||
- Reuse RequirementSchema from spec commands for consistency
|
||||
|
||||
### Delta Operations
|
||||
**Four operation types:**
|
||||
1. **ADDED**: New requirements added to specs
|
||||
2. **MODIFIED**: Changes to existing requirements
|
||||
3. **REMOVED**: Requirements being deleted
|
||||
4. **RENAMED**: Spec identifier changes
|
||||
|
||||
**Design choice:** Explicit operation types rather than diff-based approach for:
|
||||
- Human readability in markdown
|
||||
- Clear intent communication
|
||||
- Easier validation and tooling
|
||||
|
||||
### Dependency on Spec Commands
|
||||
- **Shared schemas**: RequirementSchema and ScenarioSchema reused
|
||||
- **Implementation order**: spec commands must be implemented first
|
||||
- **Common parser utilities**: Share markdown parsing logic
|
||||
|
||||
### Legacy Compatibility
|
||||
- Keep existing `list` command functional with deprecation warning
|
||||
- Migration path: `list` → `change list` with same functionality
|
||||
- Gradual transition to avoid breaking existing workflows
|
||||
@@ -0,0 +1,17 @@
|
||||
# Change: Add Change Commands with JSON Output
|
||||
|
||||
## Why
|
||||
|
||||
OpenSpec change proposals currently can only be viewed as markdown files, creating the same programmatic access limitations as specs. Additionally, the current `openspec list` command only lists changes, which is inconsistent with the new resource-based command structure.
|
||||
|
||||
## What Changes
|
||||
|
||||
- **cli-change:** Add new command for managing change proposals with show, list, and validate subcommands
|
||||
- **cli-list:** Add deprecation notice for legacy list command to guide users to the new change list command
|
||||
|
||||
## Impact
|
||||
|
||||
- **Affected specs**: cli-list (modify to add deprecation notice)
|
||||
- **Affected code**:
|
||||
- src/cli/index.ts (register new command)
|
||||
- src/core/list.ts (add deprecation notice)
|
||||
@@ -0,0 +1,48 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Change Command
|
||||
|
||||
The system SHALL provide a `change` command with subcommands for displaying, listing, and validating change proposals.
|
||||
|
||||
#### Scenario: Show change as JSON
|
||||
|
||||
- **WHEN** executing `openspec change show update-error --json`
|
||||
- **THEN** parse the markdown change file
|
||||
- **AND** extract change structure and deltas
|
||||
- **AND** output valid JSON to stdout
|
||||
|
||||
#### Scenario: List all changes
|
||||
|
||||
- **WHEN** executing `openspec change list`
|
||||
- **THEN** scan the openspec/changes directory
|
||||
- **AND** return list of all pending changes
|
||||
- **AND** support JSON output with `--json` flag
|
||||
|
||||
#### Scenario: Show only requirement changes
|
||||
|
||||
- **WHEN** executing `openspec change show update-error --requirements-only`
|
||||
- **THEN** display only the requirement changes (ADDED/MODIFIED/REMOVED/RENAMED)
|
||||
- **AND** exclude why and what changes sections
|
||||
|
||||
#### Scenario: Validate change structure
|
||||
|
||||
- **WHEN** executing `openspec change validate update-error`
|
||||
- **THEN** parse the change file
|
||||
- **AND** validate against Zod schema
|
||||
- **AND** ensure deltas are well-formed
|
||||
|
||||
### Requirement: Legacy Compatibility
|
||||
|
||||
The system SHALL maintain backward compatibility with the existing `list` command while showing deprecation notices.
|
||||
|
||||
#### Scenario: Legacy list command
|
||||
|
||||
- **WHEN** executing `openspec list`
|
||||
- **THEN** display current list of changes (existing behavior)
|
||||
- **AND** show deprecation notice: "Note: 'openspec list' is deprecated. Use 'openspec change list' instead."
|
||||
|
||||
#### Scenario: Legacy list with --all flag
|
||||
|
||||
- **WHEN** executing `openspec list --all`
|
||||
- **THEN** display all changes (existing behavior)
|
||||
- **AND** show same deprecation notice
|
||||
@@ -0,0 +1,12 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: List Command Behavior
|
||||
|
||||
The current `list` command behavior SHALL be preserved but marked as deprecated.
|
||||
|
||||
#### Scenario: Deprecation notice
|
||||
|
||||
- **WHEN** using the legacy `list` command
|
||||
- **THEN** continue to work as before
|
||||
- **AND** display deprecation notice
|
||||
- **AND** suggest using `openspec change list` instead
|
||||
@@ -0,0 +1,34 @@
|
||||
# Implementation Tasks (Phase 2: Builds on add-zod-validation)
|
||||
|
||||
## 1. Command Implementation
|
||||
- [x] 1.1 Create src/commands/change.ts
|
||||
- [x] 1.2 Import ChangeSchema and DeltaSchema from src/core/schemas/change.schema.ts
|
||||
- [x] 1.3 Import markdown parser from src/core/parsers/markdown-parser.ts
|
||||
- [x] 1.4 Import ChangeValidator from src/core/validation/validator.ts
|
||||
- [x] 1.5 Import JSON converter from src/core/converters/json-converter.ts
|
||||
- [x] 1.6 Implement show subcommand with JSON output using existing converter
|
||||
- [x] 1.7 Implement list subcommand
|
||||
- [x] 1.8 Implement validate subcommand using existing ChangeValidator
|
||||
- [x] 1.9 Add --requirements-only filtering option
|
||||
- [x] 1.10 Add --strict mode support (leveraging existing validation infrastructure)
|
||||
- [x] 1.11 Add --json flag for validation reports
|
||||
|
||||
## 2. Change-Specific Parser Extensions
|
||||
- [x] 2.1 Create src/core/parsers/change-parser.ts (extends base markdown parser)
|
||||
- [x] 2.2 Parse proposal structure (Why, What Changes sections)
|
||||
- [x] 2.3 Extract ADDED/MODIFIED/REMOVED/RENAMED sections
|
||||
- [x] 2.4 Parse delta operations within each section
|
||||
- [x] 2.5 Add tests for change parser
|
||||
|
||||
## 3. Legacy Compatibility
|
||||
- [x] 3.1 Update src/core/list.ts to add deprecation notice
|
||||
- [x] 3.2 Ensure existing list command continues to work
|
||||
- [x] 3.3 Add console warning for deprecated command usage
|
||||
|
||||
## 4. Integration
|
||||
- [x] 4.1 Register change command in src/cli/index.ts
|
||||
- [ ] 4.2 Add integration tests for all subcommands
|
||||
- [x] 4.3 Test JSON output for changes
|
||||
- [x] 4.4 Test legacy compatibility
|
||||
- [x] 4.5 Test validation with strict mode
|
||||
- [x] 4.6 Update CLI help documentation (add 'change' command to main help, document subcommands: show, list, validate)
|
||||
@@ -0,0 +1,20 @@
|
||||
## Why
|
||||
|
||||
Users frequently need to view changes and specs but must know in advance whether they're looking at a change or spec. The current subcommand structure (`change show`, `spec show`) creates friction when:
|
||||
- Users want to quickly view an item without remembering its type
|
||||
- Exploring the codebase requires switching between different show commands
|
||||
- Show commands without arguments return errors instead of helpful guidance
|
||||
|
||||
## What Changes
|
||||
|
||||
- Add new top-level `show` command for displaying changes or specs with intelligent selection
|
||||
- Support direct item display: `openspec show <item>` with automatic type detection
|
||||
- Interactive selection when no arguments provided
|
||||
- Enhance existing `change show` and `spec show` to support interactive selection (backwards compatibility)
|
||||
- Maintain all existing format options (--json, --deltas-only, --requirements, etc.)
|
||||
|
||||
## Impact
|
||||
|
||||
- New specs to create: cli-show
|
||||
- Specs to enhance: cli-change, cli-spec (for backwards compatibility)
|
||||
- Affected code: src/cli/index.ts, src/commands/show.ts (new), src/commands/spec.ts, src/commands/change.ts
|
||||
@@ -0,0 +1,23 @@
|
||||
# CLI Change Command Spec
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Interactive show selection
|
||||
|
||||
The change show command SHALL support interactive selection when no change name is provided.
|
||||
|
||||
#### Scenario: Interactive change selection for show
|
||||
|
||||
- **WHEN** executing `openspec change show` without arguments
|
||||
- **THEN** display an interactive list of available changes
|
||||
- **AND** allow the user to select a change to show
|
||||
- **AND** display the selected change content
|
||||
- **AND** maintain all existing show options (--json, --deltas-only)
|
||||
|
||||
#### Scenario: Non-interactive fallback keeps current behavior
|
||||
|
||||
- **GIVEN** stdin is not a TTY or `--no-interactive` is provided or environment variable `OPEN_SPEC_INTERACTIVE=0`
|
||||
- **WHEN** executing `openspec change show` without a change name
|
||||
- **THEN** do not prompt interactively
|
||||
- **AND** print the existing hint including available change IDs
|
||||
- **AND** set `process.exitCode = 1`
|
||||
@@ -0,0 +1,83 @@
|
||||
# CLI Show Command Spec
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Top-level show command
|
||||
|
||||
The CLI SHALL provide a top-level `show` command for displaying changes and specs with intelligent selection.
|
||||
|
||||
#### Scenario: Interactive show selection
|
||||
|
||||
- **WHEN** executing `openspec show` without arguments
|
||||
- **THEN** prompt user to select type (change or spec)
|
||||
- **AND** display list of available items for selected type
|
||||
- **AND** show the selected item's content
|
||||
|
||||
#### Scenario: Non-interactive environments do not prompt
|
||||
|
||||
- **GIVEN** stdin is not a TTY or `--no-interactive` is provided or environment variable `OPEN_SPEC_INTERACTIVE=0`
|
||||
- **WHEN** executing `openspec show` without arguments
|
||||
- **THEN** do not prompt
|
||||
- **AND** print a helpful hint with examples for `openspec show <item>` or `openspec change/spec show`
|
||||
- **AND** exit with code 1
|
||||
|
||||
#### Scenario: Direct item display
|
||||
|
||||
- **WHEN** executing `openspec show <item-name>`
|
||||
- **THEN** automatically detect if item is a change or spec
|
||||
- **AND** display the item's content
|
||||
- **AND** use appropriate formatting based on item type
|
||||
|
||||
#### Scenario: Type detection and ambiguity handling
|
||||
|
||||
- **WHEN** executing `openspec show <item-name>`
|
||||
- **THEN** if `<item-name>` uniquely matches a change or a spec, show that item
|
||||
- **AND** if it matches both, print an ambiguity error and suggest `--type change|spec` or using `openspec change show`/`openspec spec show`
|
||||
- **AND** if it matches neither, print not-found with nearest-match suggestions
|
||||
|
||||
#### Scenario: Explicit type override
|
||||
|
||||
- **WHEN** executing `openspec show --type change <item>`
|
||||
- **THEN** treat `<item>` as a change ID and show it (skipping auto-detection)
|
||||
|
||||
- **WHEN** executing `openspec show --type spec <item>`
|
||||
- **THEN** treat `<item>` as a spec ID and show it (skipping auto-detection)
|
||||
|
||||
### Requirement: Output format options
|
||||
|
||||
The show command SHALL support various output formats consistent with existing commands.
|
||||
|
||||
#### Scenario: JSON output
|
||||
|
||||
- **WHEN** executing `openspec show <item> --json`
|
||||
- **THEN** output the item in JSON format
|
||||
- **AND** include parsed metadata and structure
|
||||
- **AND** maintain format consistency with existing change/spec show commands
|
||||
|
||||
#### Scenario: Flag scoping and delegation
|
||||
|
||||
- **WHEN** showing a change or a spec via the top-level command
|
||||
- **THEN** accept common flags such as `--json`
|
||||
- **AND** pass through type-specific flags to the corresponding implementation
|
||||
- Change-only flags: `--deltas-only` (alias `--requirements-only` deprecated)
|
||||
- Spec-only flags: `--requirements`, `--no-scenarios`, `-r/--requirement`
|
||||
- **AND** ignore irrelevant flags for the detected type with a warning
|
||||
|
||||
### Requirement: Interactivity controls
|
||||
|
||||
- The CLI SHALL respect `--no-interactive` to disable prompts.
|
||||
- The CLI SHALL respect `OPEN_SPEC_INTERACTIVE=0` to disable prompts globally.
|
||||
- Interactive prompts SHALL only be shown when stdin is a TTY and interactivity is not disabled.
|
||||
|
||||
#### Scenario: Change-specific options
|
||||
|
||||
- **WHEN** showing a change with `openspec show <change-name> --deltas-only`
|
||||
- **THEN** display only the deltas in JSON format
|
||||
- **AND** maintain compatibility with existing change show options
|
||||
|
||||
#### Scenario: Spec-specific options
|
||||
|
||||
- **WHEN** showing a spec with `openspec show <spec-id> --requirements`
|
||||
- **THEN** display only requirements in JSON format
|
||||
- **AND** support other spec options (--no-scenarios, -r)
|
||||
- **AND** maintain compatibility with existing spec show options
|
||||
@@ -0,0 +1,23 @@
|
||||
# CLI Spec Command Spec
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Interactive spec show
|
||||
|
||||
The spec show command SHALL support interactive selection when no spec-id is provided.
|
||||
|
||||
#### Scenario: Interactive spec selection for show
|
||||
|
||||
- **WHEN** executing `openspec spec show` without arguments
|
||||
- **THEN** display an interactive list of available specs
|
||||
- **AND** allow the user to select a spec to show
|
||||
- **AND** display the selected spec content
|
||||
- **AND** maintain all existing show options (--json, --requirements, --no-scenarios, -r)
|
||||
|
||||
#### Scenario: Non-interactive fallback keeps current behavior
|
||||
|
||||
- **GIVEN** stdin is not a TTY or `--no-interactive` is provided or environment variable `OPEN_SPEC_INTERACTIVE=0`
|
||||
- **WHEN** executing `openspec spec show` without a spec-id
|
||||
- **THEN** do not prompt interactively
|
||||
- **AND** print the existing error message for missing spec-id
|
||||
- **AND** set non-zero exit code
|
||||
@@ -0,0 +1,142 @@
|
||||
# Implementation Tasks — Add Interactive Show Command
|
||||
|
||||
## Goals
|
||||
- Add a top-level `show` command with intelligent selection and type detection.
|
||||
- Add interactive selection to `change show` and `spec show` when no ID is provided.
|
||||
- Preserve raw-first output behavior and existing JSON formats/filters.
|
||||
- Respect `--no-interactive` and `OPEN_SPEC_INTERACTIVE=0` consistently.
|
||||
|
||||
---
|
||||
|
||||
## 1) CLI wiring
|
||||
- [x] In `src/cli/index.ts` add a top-level command: `program.command('show [item-name]')`
|
||||
- Options:
|
||||
- `--json`
|
||||
- `--type <type>` where `<type>` is `change|spec`
|
||||
- `--no-interactive`
|
||||
- Allow passing-through type-specific flags using `.allowUnknownOption(true)` so the top-level can forward flags to the underlying type handler.
|
||||
- Action: instantiate `new ShowCommand().execute(itemName, options)`.
|
||||
- [x] Update `change show` subcommand to accept `--no-interactive` and pass it to `ChangeCommand.show(...)`.
|
||||
- [x] Change `spec show` subcommand to accept optional ID (`show [spec-id]`), add `--no-interactive`, and pass to spec show implementation.
|
||||
|
||||
Acceptance:
|
||||
- `openspec show` exists and prints a helpful hint in non-interactive contexts when no args.
|
||||
- Unknown flags for other types do not crash parsing; they are warned/ignored appropriately.
|
||||
|
||||
---
|
||||
|
||||
## 2) New module: `src/commands/show.ts`
|
||||
- [x] Create `ShowCommand` with:
|
||||
- `execute(itemName?: string, options?: { json?: boolean; type?: string; noInteractive?: boolean; [k: string]: any })`
|
||||
- Interactive path when `!itemName` and interactive is enabled:
|
||||
- Prompt: "What would you like to show?" → `change` or `spec`.
|
||||
- Load available IDs for the chosen type and prompt selection.
|
||||
- Delegate to type-specific show implementation.
|
||||
- Non-interactive path when `!itemName`:
|
||||
- Print hint with examples:
|
||||
- `openspec show <item>`
|
||||
- `openspec change show`
|
||||
- `openspec spec show`
|
||||
- Exit with code 1.
|
||||
- Direct item path when `itemName` is provided:
|
||||
- Type override via `--type` takes precedence.
|
||||
- Otherwise detect using `getActiveChangeIds()` and `getSpecIds()`.
|
||||
- If ambiguous and no override: print error + suggestion to pass `--type` or use subcommands; exit code 1.
|
||||
- If unknown: print not-found with nearest-match suggestions; exit code 1.
|
||||
- On success: delegate to type-specific show.
|
||||
- [x] Flag scoping and pass-through:
|
||||
- Common: `--json` → forwarded to both types.
|
||||
- Change-only: `--deltas-only`, `--requirements-only` (deprecated alias).
|
||||
- Spec-only: `--requirements`, `--no-scenarios`, `-r/--requirement`.
|
||||
- Warn and ignore irrelevant flags for the resolved type.
|
||||
|
||||
Acceptance:
|
||||
- `openspec show <change-id> --json --deltas-only` matches `openspec change show <id> --json --deltas-only` output.
|
||||
- `openspec show <spec-id> --json --requirements` matches `openspec spec show <id> --json --requirements` output.
|
||||
- Ambiguity and not-found behaviors match the `cli-show` spec.
|
||||
|
||||
---
|
||||
|
||||
## 3) Refactor spec show into reusable API
|
||||
- [x] In `src/commands/spec.ts`, extract show logic into an exported `SpecCommand` with `show(specId?: string, options?: { json?: boolean; requirements?: boolean; scenarios?: boolean; requirement?: string; noInteractive?: boolean })`.
|
||||
- Reuse current helpers (`parseSpecFromFile`, `filterSpec`, raw-first printing).
|
||||
- Keep `registerSpecCommand` but delegate to `new SpecCommand().show(...)`.
|
||||
- [x] Update CLI spec show subcommand to optional arg and interactive behavior (see section 4).
|
||||
|
||||
Acceptance:
|
||||
- Existing `spec show` tests continue to pass.
|
||||
- New `SpecCommand.show` can be called from `ShowCommand`.
|
||||
|
||||
---
|
||||
|
||||
## 4) Backwards-compatible interactive in subcommands
|
||||
- [x] `src/commands/change.ts` → extend `show(changeName?: string, options?: { json?: boolean; requirementsOnly?: boolean; deltasOnly?: boolean; noInteractive?: boolean })`:
|
||||
- When `!changeName` and interactive enabled: prompt from `getActiveChangeIds()` and show the selected change.
|
||||
- Non-interactive fallback: keep current behavior (print available IDs + `openspec change list` hint, set `process.exitCode = 1`).
|
||||
- [x] `src/commands/spec.ts` → `SpecCommand.show` as above:
|
||||
- When `!specId` and interactive enabled: prompt from `getSpecIds()` and show the selected spec.
|
||||
- Non-interactive fallback: print the same error as existing behavior for missing `<spec-id>` and set non-zero exit code.
|
||||
|
||||
Acceptance:
|
||||
- `openspec change show` in non-interactive prints list hint and exits non-zero.
|
||||
- `openspec spec show` in non-interactive prints missing-arg error and exits non-zero.
|
||||
|
||||
---
|
||||
|
||||
## 5) Shared utilities
|
||||
- [x] Extract `nearestMatches` and `levenshtein` from `src/commands/validate.ts` into `src/utils/match.ts` (exported helpers).
|
||||
- [x] Update `ValidateCommand` and new `ShowCommand` to import from `utils/match`.
|
||||
|
||||
Acceptance:
|
||||
- Build succeeds with shared helpers and no duplication.
|
||||
|
||||
---
|
||||
|
||||
## 6) Hints, warnings, and messages
|
||||
- [x] Top-level `show` hint (non-interactive no-arg):
|
||||
- Lines include: `openspec show <item>`, `openspec change show`, `openspec spec show`, and "Or run in an interactive terminal.".
|
||||
- [x] Ambiguity message suggests `--type change|spec` and the subcommands.
|
||||
- [x] Not-found suggests nearest matches (up to 5).
|
||||
- [x] Irrelevant flag warnings for the resolved type (printed to stderr, no crash).
|
||||
|
||||
Acceptance:
|
||||
- Messages match the `cli-show` spec wording intent and style used elsewhere.
|
||||
|
||||
---
|
||||
|
||||
## 7) Tests
|
||||
Add tests mirroring existing patterns (non-TTY simulation via `OPEN_SPEC_INTERACTIVE=0`).
|
||||
|
||||
- [x] `test/commands/show.test.ts`
|
||||
- Non-interactive, no arg → prints hint and exits non-zero.
|
||||
- Direct item detection for change and for spec.
|
||||
- Ambiguity case when both exist → error and suggestion for `--type`.
|
||||
- Not-found case → nearest-match suggestions.
|
||||
- Pass-through flags: change `--json --deltas-only`, spec `--json --requirements`.
|
||||
- [x] `test/commands/change.interactive-show.test.ts` (non-interactive fallback)
|
||||
- Ensure `openspec change show` without args prints available IDs + list hint and non-zero exit.
|
||||
- [x] `test/commands/spec.interactive-show.test.ts` (non-interactive fallback)
|
||||
- Ensure `openspec spec show` without args prints missing-arg error and non-zero exit.
|
||||
|
||||
Acceptance:
|
||||
- All new tests pass after build; no regressions in existing tests.
|
||||
|
||||
---
|
||||
|
||||
## 8) Documentation (optional but recommended)
|
||||
- [x] Update `openspec/README.md` usage examples to include the new `show` command with type detection and flags.
|
||||
|
||||
---
|
||||
|
||||
## 9) Non-functional checks
|
||||
- [x] Run `pnpm build` and all tests (`pnpm test`).
|
||||
- [x] Ensure no linter/type errors and messages are consistent with existing style.
|
||||
|
||||
---
|
||||
|
||||
## Notes on consistency
|
||||
- Follow raw-first behavior for text output: passthrough file content with no formatting, mirroring current `change show` and `spec show`.
|
||||
- Reuse `isInteractive` and `item-discovery` helpers for consistent prompting behavior.
|
||||
- Keep JSON output shapes identical to current `ChangeCommand.show` and `spec show` outputs.
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
## Why
|
||||
The archive command currently forces users to either accept spec updates or cancel the entire archive operation. Users need flexibility to archive changes without updating specs, either through explicit flags or by declining the confirmation prompt. This is especially important for changes that don't modify specs (like tooling, documentation, or infrastructure updates).
|
||||
|
||||
## What Changes
|
||||
- Add new `--skip-specs` flag to the archive command that bypasses all spec update operations
|
||||
- Fix confirmation behavior: when users decline spec updates interactively, proceed with archiving instead of cancelling the entire operation
|
||||
- When `--skip-specs` flag is used, skip both the spec discovery and update confirmation steps entirely
|
||||
- Display clear message when specs are skipped (either via flag or user choice)
|
||||
- Flag can be combined with existing `--yes` flag for fully automated archiving without spec updates
|
||||
|
||||
## Impact
|
||||
- Affected specs: cli-archive
|
||||
- Affected code: src/core/archive.ts, src/cli/index.ts
|
||||
@@ -0,0 +1,191 @@
|
||||
# CLI Archive Command Specification
|
||||
|
||||
## Purpose
|
||||
The archive command moves completed changes from the active changes directory to the archive folder with date-based naming, following OpenSpec conventions.
|
||||
|
||||
## Command Syntax
|
||||
```bash
|
||||
openspec archive [change-name] [--yes|-y] [--skip-specs]
|
||||
```
|
||||
|
||||
Options:
|
||||
- `--yes`, `-y`: Skip confirmation prompts (for automation)
|
||||
- `--skip-specs`: Skip spec update operations entirely (for changes without spec modifications)
|
||||
|
||||
## Behavior
|
||||
|
||||
### Requirement: Change Selection
|
||||
|
||||
The command SHALL support both interactive and direct change selection methods.
|
||||
|
||||
#### Scenario: Interactive selection
|
||||
|
||||
- **WHEN** no change-name is provided
|
||||
- **THEN** display interactive list of available changes (excluding archive/)
|
||||
- **AND** allow user to select one
|
||||
|
||||
#### Scenario: Direct selection
|
||||
|
||||
- **WHEN** change-name is provided
|
||||
- **THEN** use that change directly
|
||||
- **AND** validate it exists
|
||||
|
||||
### Requirement: Task Completion Check
|
||||
|
||||
The command SHALL verify task completion status before archiving to prevent premature archival.
|
||||
|
||||
#### Scenario: Incomplete tasks found
|
||||
|
||||
- **WHEN** incomplete tasks are found (marked with `- [ ]`)
|
||||
- **THEN** display all incomplete tasks to the user
|
||||
- **AND** prompt for confirmation to continue
|
||||
- **AND** default to "No" for safety
|
||||
|
||||
#### Scenario: All tasks complete
|
||||
|
||||
- **WHEN** all tasks are complete OR no tasks.md exists
|
||||
- **THEN** proceed with archiving without prompting
|
||||
|
||||
### Requirement: Archive Process
|
||||
|
||||
The archive operation SHALL follow a structured process to safely move changes to the archive.
|
||||
|
||||
#### Scenario: Performing archive
|
||||
|
||||
- **WHEN** archiving a change
|
||||
- **THEN** execute these steps:
|
||||
1. Create archive/ directory if it doesn't exist
|
||||
2. Generate target name as `YYYY-MM-DD-[change-name]` using current date
|
||||
3. Check if target directory already exists
|
||||
4. Update main specs from the change's future state specs unless `--skip-specs` is provided (see Spec Update Process below)
|
||||
5. Move the entire change directory to the archive location
|
||||
|
||||
#### Scenario: Archive already exists
|
||||
|
||||
- **WHEN** target archive already exists
|
||||
- **THEN** fail with error message
|
||||
- **AND** do not overwrite existing archive
|
||||
|
||||
#### Scenario: Successful archive
|
||||
|
||||
- **WHEN** move succeeds
|
||||
- **THEN** display success message with archived name and list of updated specs (if any)
|
||||
|
||||
### Requirement: Spec Update Process
|
||||
|
||||
Before moving the change to archive, the command SHALL update main specs to reflect the deployed reality unless the `--skip-specs` flag is provided.
|
||||
|
||||
#### Scenario: Skipping spec updates
|
||||
|
||||
- **WHEN** the `--skip-specs` flag is provided
|
||||
- **THEN** skip all spec discovery and update operations
|
||||
- **AND** proceed directly to moving the change to archive
|
||||
- **AND** display message indicating specs were skipped
|
||||
|
||||
#### Scenario: Updating specs from change
|
||||
|
||||
- **WHEN** the change contains specs in `changes/[name]/specs/` AND `--skip-specs` is NOT provided
|
||||
- **THEN** execute these steps:
|
||||
1. Analyze which specs will be affected by comparing with existing specs
|
||||
2. Display a summary of spec updates to the user (see Confirmation Behavior below)
|
||||
3. Prompt for confirmation unless `--yes` flag is provided
|
||||
4. If confirmed, for each capability spec in the change directory:
|
||||
- Copy the spec from `changes/[name]/specs/[capability]/spec.md` to `openspec/specs/[capability]/spec.md`
|
||||
- Create the target directory structure if it doesn't exist
|
||||
- Overwrite existing spec files (specs represent current reality, change specs are the new reality)
|
||||
- Track which specs were updated for the success message
|
||||
|
||||
#### Scenario: No specs in change
|
||||
|
||||
- **WHEN** no specs exist in the change AND `--skip-specs` is NOT provided
|
||||
- **THEN** skip the spec update step
|
||||
- **AND** proceed with archiving
|
||||
|
||||
### Requirement: Confirmation Behavior
|
||||
|
||||
The spec update confirmation SHALL provide clear visibility into changes before they are applied.
|
||||
|
||||
#### Scenario: Displaying confirmation
|
||||
|
||||
- **WHEN** prompting for confirmation AND `--skip-specs` is NOT provided
|
||||
- **THEN** display a clear summary showing:
|
||||
- Which specs will be created (new capabilities)
|
||||
- Which specs will be updated (existing capabilities)
|
||||
- The source path for each spec
|
||||
- **AND** format the confirmation prompt as:
|
||||
```
|
||||
The following specs will be updated:
|
||||
|
||||
NEW specs to be created:
|
||||
- cli-archive (from changes/add-archive-command/specs/cli-archive/spec.md)
|
||||
|
||||
EXISTING specs to be updated:
|
||||
- cli-init (from changes/update-init-command/specs/cli-init/spec.md)
|
||||
|
||||
Update 2 specs and archive 'add-archive-command'? [y/N]:
|
||||
```
|
||||
#### Scenario: Handling confirmation response
|
||||
|
||||
- **WHEN** waiting for user confirmation
|
||||
- **THEN** default to "No" for safety (require explicit "y" or "yes")
|
||||
- **AND** skip confirmation when `--yes` or `-y` flag is provided
|
||||
- **AND** skip entire spec confirmation when `--skip-specs` flag is provided
|
||||
|
||||
#### Scenario: User declines spec update confirmation
|
||||
|
||||
- **WHEN** user declines the spec update confirmation
|
||||
- **THEN** skip the spec update operations
|
||||
- **AND** display message: "Skipping spec updates. Proceeding with archive."
|
||||
- **AND** continue with the archive operation
|
||||
- **AND** display success message indicating specs were not updated
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Requirement: Error Conditions
|
||||
|
||||
The command SHALL handle various error conditions gracefully.
|
||||
|
||||
#### Scenario: Handling errors
|
||||
|
||||
- **WHEN** errors occur
|
||||
- **THEN** handle the following conditions:
|
||||
- Missing openspec/changes/ directory
|
||||
- Change not found
|
||||
- Archive target already exists
|
||||
- File system permissions issues
|
||||
|
||||
## Why These Decisions
|
||||
|
||||
**Interactive selection**: Reduces typing and helps users see available changes
|
||||
**Task checking**: Prevents accidental archiving of incomplete work
|
||||
**Date prefixing**: Maintains chronological order and prevents naming conflicts
|
||||
**No overwrite**: Preserves historical archives and prevents data loss
|
||||
**Spec updates before archiving**: Specs in the main directory represent current reality; when a change is deployed and archived, its future state specs become the new reality and must replace the main specs
|
||||
**Confirmation for spec updates**: Provides visibility into what will change, prevents accidental overwrites, and ensures users understand the impact before specs are modified
|
||||
**Non-blocking confirmation**: Declining spec updates doesn't cancel archiving - users can review specs and choose to update them separately if needed
|
||||
**--yes flag for automation**: Allows CI/CD pipelines to archive without interactive prompts while maintaining safety by default for manual use
|
||||
**--skip-specs flag**: Enables archiving of changes that don't modify specs (like infrastructure, tooling, or documentation changes) without unnecessary spec update prompts or operations
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Skip Specs Option
|
||||
|
||||
The archive command SHALL support a `--skip-specs` flag that skips all spec update operations and proceeds directly to archiving.
|
||||
|
||||
#### Scenario: Skipping spec updates with flag
|
||||
|
||||
- **WHEN** executing `openspec archive <change> --skip-specs`
|
||||
- **THEN** skip spec discovery and update confirmation
|
||||
- **AND** proceed directly to moving the change to archive
|
||||
- **AND** display a message indicating specs were skipped
|
||||
|
||||
### Requirement: Non-blocking confirmation
|
||||
|
||||
The archive operation SHALL proceed when the user declines spec updates instead of cancelling the entire operation.
|
||||
|
||||
#### Scenario: User declines spec update confirmation
|
||||
|
||||
- **WHEN** the user declines spec update confirmation
|
||||
- **THEN** skip spec updates
|
||||
- **AND** continue with the archive operation
|
||||
- **AND** display a success message indicating specs were not updated
|
||||
@@ -0,0 +1,57 @@
|
||||
## 1. Update Archive Command Implementation
|
||||
- [x] 1.1 Add `skipSpecs` option to the archive command options interface
|
||||
- [x] 1.2 Modify the execute method to skip spec operations when flag is set
|
||||
- [x] 1.3 Fix confirmation behavior: when user declines spec updates, proceed with archiving instead of cancelling
|
||||
- [x] 1.4 Update console output to indicate when specs are being skipped (via flag or user choice)
|
||||
- [x] 1.5 Ensure archive continues after declining spec updates
|
||||
|
||||
## 2. Update CLI Interface
|
||||
- [x] 2.1 Add `--skip-specs` flag to the archive command definition
|
||||
- [x] 2.2 Pass the flag value to the archive command execute method
|
||||
|
||||
## 3. Update Tests
|
||||
- [x] 3.1 Add test case for archiving with --skip-specs flag
|
||||
- [x] 3.2 Add test case for declining spec updates but continuing with archive
|
||||
- [x] 3.3 Verify that spec updates are skipped when flag is used
|
||||
- [x] 3.4 Verify that archive proceeds when user declines spec updates
|
||||
- [x] 3.5 Ensure existing behavior remains unchanged when flag is not used
|
||||
|
||||
## 4. Update Documentation
|
||||
- [x] 4.1 Update the cli-archive spec to document the new --skip-specs flag
|
||||
- [x] 4.2 Document the new behavior when declining spec updates interactively
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
### Key Design Decisions
|
||||
|
||||
1. **Non-blocking Confirmation Behavior**: When users decline spec updates interactively, the archive operation continues rather than cancelling entirely. This was a critical UX improvement because:
|
||||
- Users may want to review specs separately before updating them
|
||||
- Archiving work shouldn't be blocked by spec review decisions
|
||||
- Maintains flexibility in the deployment workflow
|
||||
|
||||
2. **Flag Naming Convention**: Chose `--skip-specs` for clarity and consistency:
|
||||
- Clearly indicates the action (skipping) and target (specs)
|
||||
- Follows kebab-case convention for CLI flags
|
||||
- Converts naturally to `skipSpecs` camelCase in code
|
||||
|
||||
3. **Console Messaging Strategy**: Added explicit messages for all spec-skipping scenarios:
|
||||
- When flag is used: "Skipping spec updates (--skip-specs flag provided)."
|
||||
- When user declines: "Skipping spec updates. Proceeding with archive."
|
||||
- Ensures users always understand what's happening with their specs
|
||||
|
||||
4. **Test Coverage Approach**: Created separate test cases for:
|
||||
- Flag-based skipping (explicit user choice via CLI)
|
||||
- Interactive declining (runtime user decision)
|
||||
- Both verify the same outcome but test different code paths
|
||||
|
||||
### Use Cases Addressed
|
||||
|
||||
- **Infrastructure Changes**: Changes to build tools, CI/CD, dependencies
|
||||
- **Documentation Updates**: README updates, comment improvements
|
||||
- **Tooling Modifications**: Developer tools, scripts, configuration files
|
||||
- **Refactoring**: Code improvements that don't change functionality/specs
|
||||
|
||||
### Future Considerations
|
||||
|
||||
- Could potentially auto-detect when changes don't include specs and suggest using the flag
|
||||
- May want to track which archives skipped spec updates for audit purposes
|
||||
@@ -0,0 +1,45 @@
|
||||
# Design: Spec Commands
|
||||
|
||||
## Architecture Decisions
|
||||
|
||||
### Command Hierarchy
|
||||
We chose a subcommand pattern (`spec show`, `spec list`, `spec validate`) to:
|
||||
- Group related functionality under a common namespace
|
||||
- Enable future extensibility without polluting the top-level CLI
|
||||
- Maintain consistency with the planned `change` command structure
|
||||
|
||||
### JSON Schema Structure
|
||||
The spec JSON schema follows this structure:
|
||||
```typescript
|
||||
{
|
||||
version: string, // Schema version for compatibility
|
||||
format: "spec", // Identifies this as a spec document
|
||||
sourcePath: string, // Original markdown file path
|
||||
id: string, // Spec identifier from filename
|
||||
title: string, // Human-readable title
|
||||
overview?: string, // Optional overview section
|
||||
requirements: Array<{
|
||||
id: string,
|
||||
text: string,
|
||||
scenarios: Array<{
|
||||
id: string,
|
||||
text: string
|
||||
}>
|
||||
}>
|
||||
}
|
||||
```
|
||||
|
||||
**Rationale:**
|
||||
- Flat structure for requirements array (vs nested objects) for easier iteration
|
||||
- Scenarios nested within requirements to maintain relationship
|
||||
- Metadata fields (version, format, sourcePath) for tooling integration
|
||||
|
||||
### Parser Architecture
|
||||
- **Markdown-first approach**: Parse markdown headings rather than custom syntax
|
||||
- **Streaming parser**: Process line-by-line to handle large files efficiently
|
||||
- **Strict heading hierarchy**: Enforce ##/###/#### structure for consistency
|
||||
|
||||
### Validation Strategy
|
||||
- **Parse-time validation**: Catch structural issues during parsing
|
||||
- **Schema validation**: Use Zod for runtime type checking of parsed data
|
||||
- **Separate validation command**: Allow validation without full parsing/conversion
|
||||
@@ -0,0 +1,19 @@
|
||||
# Change: Add Spec Commands with JSON Output
|
||||
|
||||
## Why
|
||||
|
||||
Currently, OpenSpec specs can only be viewed as markdown files. This makes programmatic access difficult and prevents integration with CI/CD pipelines, external tools, and automated processing.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Add new `openspec spec` command with three subcommands: `show`, `list`, and `validate`
|
||||
- Implement JSON output capability for specs using heading-based parsing
|
||||
- Add Zod schemas for spec structure validation
|
||||
- Enable content filtering options (requirements only, no scenarios, specific requirement)
|
||||
|
||||
## Impact
|
||||
|
||||
- **Affected specs**: None (new capability)
|
||||
- **Affected code**:
|
||||
- src/cli/index.ts (register new command)
|
||||
- package.json (add zod dependency)
|
||||
@@ -0,0 +1,43 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Spec Command
|
||||
|
||||
The system SHALL provide a `spec` command with subcommands for displaying, listing, and validating specifications.
|
||||
|
||||
#### Scenario: Show spec as JSON
|
||||
|
||||
- **WHEN** executing `openspec spec show init --json`
|
||||
- **THEN** parse the markdown spec file
|
||||
- **AND** extract headings and content hierarchically
|
||||
- **AND** output valid JSON to stdout
|
||||
|
||||
#### Scenario: List all specs
|
||||
|
||||
- **WHEN** executing `openspec spec list`
|
||||
- **THEN** scan the openspec/specs directory
|
||||
- **AND** return list of all available capabilities
|
||||
- **AND** support JSON output with `--json` flag
|
||||
|
||||
#### Scenario: Filter spec content
|
||||
|
||||
- **WHEN** executing `openspec spec show init --requirements`
|
||||
- **THEN** display only requirement names and SHALL statements
|
||||
- **AND** exclude scenario content
|
||||
|
||||
#### Scenario: Validate spec structure
|
||||
|
||||
- **WHEN** executing `openspec spec validate init`
|
||||
- **THEN** parse the spec file
|
||||
- **AND** validate against Zod schema
|
||||
- **AND** report any structural issues
|
||||
|
||||
### Requirement: JSON Schema Definition
|
||||
|
||||
The system SHALL define Zod schemas that accurately represent the spec structure for runtime validation.
|
||||
|
||||
#### Scenario: Schema validation
|
||||
|
||||
- **WHEN** parsing a spec into JSON
|
||||
- **THEN** validate the structure using Zod schemas
|
||||
- **AND** ensure all required fields are present
|
||||
- **AND** provide clear error messages for validation failures
|
||||
@@ -0,0 +1,22 @@
|
||||
# Implementation Tasks (Phase 3: Builds on add-zod-validation and add-change-commands)
|
||||
|
||||
## 1. Command Implementation
|
||||
- [x] 1.1 Create src/commands/spec.ts
|
||||
- [x] 1.2 Import RequirementSchema, ScenarioSchema, SpecSchema from src/core/schemas/
|
||||
- [x] 1.3 Import markdown parser from src/core/parsers/markdown-parser.ts
|
||||
- [x] 1.4 Import SpecValidator from src/core/validation/validator.ts
|
||||
- [x] 1.5 Import JSON converter from src/core/converters/json-converter.ts
|
||||
- [x] 1.6 Implement show subcommand with JSON output using existing converter
|
||||
- [x] 1.7 Implement list subcommand
|
||||
- [x] 1.8 Implement validate subcommand using existing SpecValidator
|
||||
- [x] 1.9 Add filtering options (--requirements, --no-scenarios, -r)
|
||||
- [x] 1.10 Add --strict mode support (leveraging existing validation infrastructure)
|
||||
- [x] 1.11 Add --json flag for validation reports
|
||||
|
||||
## 2. Integration
|
||||
- [x] 2.1 Register spec command in src/cli/index.ts
|
||||
- [x] 2.2 Add integration tests for all subcommands
|
||||
- [x] 2.3 Test JSON output validation
|
||||
- [x] 2.4 Test filtering options
|
||||
- [x] 2.5 Test validation with strict mode
|
||||
- [x] 2.6 Update CLI help documentation (add 'spec' command to main help, document subcommands: show, list, validate)
|
||||
@@ -0,0 +1,104 @@
|
||||
# Design: Zod Validation Framework
|
||||
|
||||
## Architecture Decisions
|
||||
|
||||
### Validation Levels
|
||||
Three-tier validation system:
|
||||
1. **ERROR**: Structural issues that prevent parsing (must fix)
|
||||
2. **WARNING**: Quality issues that should be addressed (recommended fix)
|
||||
3. **INFO**: Suggestions for improvement (optional)
|
||||
|
||||
**Rationale:**
|
||||
- Gradual enforcement allows teams to adopt validation incrementally
|
||||
- CI/CD can fail on errors but allow warnings initially
|
||||
- Info level provides guidance without blocking
|
||||
|
||||
### Validation Rules Hierarchy
|
||||
|
||||
#### Spec Validation Rules
|
||||
```
|
||||
ERROR level:
|
||||
- Missing ## Overview or ## Requirements sections
|
||||
- Invalid heading hierarchy
|
||||
- Malformed requirement/scenario structure
|
||||
|
||||
WARNING level:
|
||||
- Requirements without scenarios
|
||||
- Requirements missing SHALL keyword
|
||||
- Empty overview section
|
||||
|
||||
INFO level:
|
||||
- Very long requirement text (>500 chars)
|
||||
- Scenarios without Given/When/Then structure
|
||||
```
|
||||
|
||||
#### Change Validation Rules
|
||||
```
|
||||
ERROR level:
|
||||
- Missing ## Why or ## What Changes sections
|
||||
- Invalid delta operation types
|
||||
- Malformed delta structure
|
||||
|
||||
WARNING level:
|
||||
- Why section too brief (<50 chars)
|
||||
- Deltas without clear descriptions
|
||||
- Missing requirements in ADDED/MODIFIED
|
||||
|
||||
INFO level:
|
||||
- Very long why section (>1000 chars)
|
||||
- Too many deltas in single change (>10)
|
||||
```
|
||||
|
||||
### Strict Mode
|
||||
- **Default**: Show all levels, fail on ERROR only
|
||||
- **--strict flag**: Fail on both ERROR and WARNING
|
||||
- **Use case**: Gradual quality improvement in CI/CD pipelines
|
||||
|
||||
### Archive Command Safety
|
||||
**Problem:** Invalid specs could be archived, polluting the archive.
|
||||
|
||||
**Solution:**
|
||||
1. Pre-archive validation (default behavior)
|
||||
2. --no-validate flag with safeguards:
|
||||
- Interactive confirmation prompt
|
||||
- Prominent warning message
|
||||
- Console logging with timestamp
|
||||
- Not recommended for CI/CD usage
|
||||
|
||||
**Rationale:**
|
||||
- Protect archive integrity by default
|
||||
- Allow emergency overrides with accountability
|
||||
- Clear audit trail for validation bypasses
|
||||
|
||||
### Validation Report Format
|
||||
```json
|
||||
{
|
||||
"valid": boolean,
|
||||
"issues": [
|
||||
{
|
||||
"level": "ERROR" | "WARNING" | "INFO",
|
||||
"path": "requirements[0].scenarios",
|
||||
"message": "Requirement must have at least one scenario",
|
||||
"line": 15,
|
||||
"column": 0
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"errors": 2,
|
||||
"warnings": 5,
|
||||
"info": 3
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Machine-readable for tooling integration
|
||||
- Human-friendly messages
|
||||
- Line/column info for IDE integration
|
||||
- Summary for quick assessment
|
||||
|
||||
### Implementation Strategy
|
||||
1. **Zod schemas with refinements**: Built-in validation in type definitions
|
||||
2. **Custom validators**: Additional business logic validation
|
||||
3. **Composable rules**: Mix and match for different contexts
|
||||
4. **Extensible framework**: Easy to add new rules without refactoring
|
||||
@@ -0,0 +1,22 @@
|
||||
# Change: Add Zod Runtime Validation
|
||||
|
||||
## Why
|
||||
|
||||
While the spec and change commands can output JSON, they currently don't perform strict runtime validation beyond basic structure checking. This can lead to invalid specs or changes being processed, silent failures when required fields are missing, and poor error messages.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Enhance existing `spec validate` and `change validate` commands with strict Zod validation
|
||||
- Add validation to the archive command to ensure changes are valid before applying
|
||||
- Add validation to the diff command to ensure changes are well-formed
|
||||
- Provide detailed validation reports in JSON format
|
||||
- Add `--strict` mode that fails on warnings
|
||||
|
||||
## Impact
|
||||
|
||||
- **Affected specs**: cli-spec, cli-change, cli-archive, cli-diff
|
||||
- **Affected code**:
|
||||
- src/commands/spec.ts (enhance validate subcommand)
|
||||
- src/commands/change.ts (enhance validate subcommand)
|
||||
- src/core/archive.ts (add pre-archive validation)
|
||||
- src/core/diff.ts (add validation check)
|
||||
@@ -0,0 +1,18 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Archive Validation
|
||||
|
||||
The archive command SHALL validate changes before applying them to ensure data integrity.
|
||||
|
||||
#### Scenario: Pre-archive validation
|
||||
|
||||
- **WHEN** executing `openspec archive change-name`
|
||||
- **THEN** validate the change structure first
|
||||
- **AND** only proceed if validation passes
|
||||
- **AND** show validation errors if it fails
|
||||
|
||||
#### Scenario: Force archive without validation
|
||||
|
||||
- **WHEN** executing `openspec archive change-name --no-validate`
|
||||
- **THEN** skip validation (unsafe mode)
|
||||
- **AND** show warning about skipping validation
|
||||
@@ -0,0 +1,12 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Diff Command Enhancement
|
||||
|
||||
The diff command SHALL validate change structure before displaying differences.
|
||||
|
||||
#### Scenario: Validate before diff
|
||||
|
||||
- **WHEN** executing `openspec diff change-name`
|
||||
- **THEN** validate change structure
|
||||
- **AND** show validation warnings if present
|
||||
- **AND** continue with diff display
|
||||
@@ -0,0 +1,59 @@
|
||||
# Implementation Tasks (Foundation Phase)
|
||||
|
||||
## 1. Core Schemas
|
||||
- [x] 1.1 Add zod dependency to package.json
|
||||
- [x] 1.2 Create src/core/schemas/base.schema.ts with ScenarioSchema and RequirementSchema
|
||||
- [x] 1.3 Create src/core/schemas/spec.schema.ts with SpecSchema
|
||||
- [x] 1.4 Create src/core/schemas/change.schema.ts with DeltaSchema and ChangeSchema
|
||||
- [x] 1.5 Create src/core/schemas/index.ts to export all schemas
|
||||
|
||||
## 2. Parser Implementation
|
||||
- [x] 2.1 Create src/core/parsers/markdown-parser.ts
|
||||
- [x] 2.2 Implement heading extraction (##, ###, ####)
|
||||
- [x] 2.3 Implement content capture between headings
|
||||
- [x] 2.4 Add tests for parser edge cases
|
||||
|
||||
## 3. Validation Infrastructure
|
||||
- [x] 3.1 Create src/core/validation/types.ts with ValidationLevel, ValidationIssue, ValidationReport types
|
||||
- [x] 3.2 Create src/core/validation/constants.ts with validation rules and thresholds
|
||||
- [x] 3.3 Create src/core/validation/validator.ts with SpecValidator and ChangeValidator classes
|
||||
|
||||
## 4. Enhanced Validation Rules
|
||||
- [x] 4.1 Add RequirementValidation refinements (must have scenarios, must contain SHALL)
|
||||
- [x] 4.2 Add SpecValidation refinements (must have requirements)
|
||||
- [x] 4.3 Add ChangeValidation refinements (must have deltas, why section length)
|
||||
- [x] 4.4 Implement custom error messages for each rule
|
||||
|
||||
## 5. JSON Converter
|
||||
- [x] 5.1 Create src/core/converters/json-converter.ts
|
||||
- [x] 5.2 Implement spec-to-JSON conversion
|
||||
- [x] 5.3 Implement change-to-JSON conversion
|
||||
- [x] 5.4 Add metadata fields (version, format, sourcePath)
|
||||
|
||||
## 6. Archive Command Enhancement
|
||||
- [x] 6.1 Add pre-archive validation check using new validators
|
||||
- [x] 6.2 Add --no-validate flag with required confirmation prompt and warning message: "⚠️ WARNING: Skipping validation may archive invalid specs. Continue? (y/N)"
|
||||
- [x] 6.3 Display validation errors before aborting
|
||||
- [x] 6.4 Log all --no-validate usages to console with timestamp and affected files
|
||||
- [x] 6.5 Add tests for validation scenarios including --no-validate confirmation flow
|
||||
|
||||
## 7. Diff Command Enhancement
|
||||
- [x] 7.1 Add validation check before diff using new validators
|
||||
- [x] 7.2 Show validation warnings (non-blocking)
|
||||
- [x] 7.3 Continue with diff even if warnings present
|
||||
|
||||
## 8. Testing
|
||||
- [x] 8.1 Unit tests for all schemas
|
||||
- [x] 8.2 Unit tests for parser
|
||||
- [x] 8.3 Unit tests for validation rules
|
||||
- [x] 8.4 Integration tests for validation reports
|
||||
- [x] 8.5 Test various invalid spec/change formats
|
||||
- [x] 8.6 Test strict mode behavior
|
||||
- [x] 8.7 Test pre-archive validation
|
||||
- [x] 8.8 Test validation report JSON output
|
||||
|
||||
## 9. Documentation
|
||||
- [x] 9.1 Document schema structure and validation rules (openspec/VALIDATION.md)
|
||||
- [x] 9.2 Update CLI help for archive (document --no-validate flag and its warnings)
|
||||
- [x] 9.3 Update CLI help for diff (document validation warnings behavior)
|
||||
- [x] 9.4 Create migration guide for future command integration (openspec/MIGRATION.md)
|
||||
@@ -0,0 +1,93 @@
|
||||
# Adopt Delta-Based Changes for Specifications
|
||||
|
||||
## Why
|
||||
|
||||
The current approach of storing complete future states in change proposals creates a poor review experience. When reviewing changes on GitHub, reviewers see entire spec files (often 100+ lines) as "added" in green, making it impossible to identify what actually changed. With the recent structured format adoption, we now have clear section boundaries that enable a better approach: storing only additions and modifications.
|
||||
|
||||
## What Changes
|
||||
|
||||
Store only the requirements that actually change, not complete future states:
|
||||
|
||||
- **ADDED Requirements**: New capabilities being introduced
|
||||
- **MODIFIED Requirements**: Existing requirements being changed (must match current header)
|
||||
- **REMOVED Requirements**: Deprecated capabilities
|
||||
- **RENAMED Requirements**: Explicit header changes (e.g., `FROM: Old Name` → `TO: New Name`)
|
||||
|
||||
The archive command will programmatically apply these deltas using normalized header matching (trim leading/trailing whitespace) instead of manually copying entire files.
|
||||
|
||||
## Impact
|
||||
|
||||
**Affected specs**: openspec-conventions, cli-archive, cli-diff
|
||||
|
||||
**Benefits**:
|
||||
- GitHub diffs show only actual changes (25 lines instead of 150+)
|
||||
- Reviewers immediately see what's being added, modified, or removed
|
||||
- Conflicts are more apparent when two changes modify the same requirement
|
||||
- Archive command can programmatically apply changes
|
||||
|
||||
**Format**: Delta format only - all changes must use ADDED/MODIFIED/REMOVED sections.
|
||||
|
||||
## Example
|
||||
|
||||
Instead of storing a 150-line complete future spec, store only:
|
||||
|
||||
```markdown
|
||||
# User Authentication - Changes
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: OAuth Support
|
||||
Users SHALL authenticate via OAuth providers including Google and GitHub.
|
||||
|
||||
#### Scenario: OAuth login flow
|
||||
- **WHEN** user selects OAuth provider
|
||||
- **THEN** redirect to provider authorization
|
||||
- **AND** exchange authorization code for tokens
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Session Management
|
||||
Sessions SHALL expire after 30 minutes of inactivity.
|
||||
|
||||
#### Scenario: Inactive session timeout
|
||||
- **WHEN** no activity for 30 minutes ← (was 60 minutes)
|
||||
- **THEN** invalidate session token
|
||||
- **AND** require re-authentication
|
||||
|
||||
## RENAMED Requirements
|
||||
- FROM: `### Requirement: Basic Authentication`
|
||||
- TO: `### Requirement: Email Authentication`
|
||||
```
|
||||
|
||||
This makes reviews focused and changes explicit.
|
||||
|
||||
## Conflict Resolution
|
||||
|
||||
Git naturally detects conflicts when two changes modify the same requirement header. This is actually better than full-state storage where Git might silently merge incompatible changes.
|
||||
|
||||
## Decisions and Product Guidelines
|
||||
|
||||
To keep the archive flow lean and predictable, the following decisions apply:
|
||||
|
||||
- New spec creation: When a target spec does not exist, auto-generate a minimal skeleton and insert ADDED requirements only. Skeleton format:
|
||||
- `# [Spec Name] Specification`
|
||||
- `## Purpose` with placeholder: "TBD — created by archiving change [change-name]. Update Purpose after archive."
|
||||
- `## Requirements`
|
||||
- If a non-existent spec includes MODIFIED/REMOVED/RENAMED, abort with guidance to create via ADDED-only first.
|
||||
|
||||
- Requirement identification: Match requirements by exact header `### Requirement: [Name]` with trim-only normalization and case-sensitive comparison. Use a requirement-block extractor that preserves the exact header and captures full content (including scenarios) for both main specs and delta files.
|
||||
|
||||
- Application order and atomicity: Apply deltas in order RENAMED → REMOVED → MODIFIED → ADDED. Validate all operations first, apply in-memory, and write each spec once. On any validation failure, abort without writing partial results. An aggregated totals line is displayed across all specs: `Totals: + A, ~ M, - R, → N`.
|
||||
|
||||
- Validation matrix: Enforce that MODIFIED/REMOVED exist; ADDED do not exist; RENAMED FROM exists and TO does not; no duplicates after all operations; and no cross-section conflicts (e.g., same item in MODIFIED and REMOVED). When a rename and modify apply to the same item, MODIFIED must reference the NEW header.
|
||||
|
||||
- Idempotency: Keep v1 simple. Abort on precondition failures (e.g., ADDED already exists) with clear errors. Do not implement no-op detection in v1.
|
||||
|
||||
- Output and UX: For each spec, display operation counts using standard symbols `+ ~ - →`. Optionally include a short aggregated totals line at the end. Keep messages concise and actionable.
|
||||
|
||||
- Error messaging: Standardize messages as `[spec] [operation] failed for header "### Requirement: X" — reason`. On abort, explicitly state: `Aborted. No files were changed.`
|
||||
- Subsections: Any subsections under a requirement (e.g., `#### Scenario: ...`) are preserved verbatim during parsing and application.
|
||||
|
||||
- Backward compatibility: Reject full future-state spec copies for existing specs with guidance to convert to deltas. Allow brand-new specs to be created via ADDED-only deltas using the skeleton above.
|
||||
|
||||
- Dry-run: Deferred for v1 to keep scope minimal.
|
||||
@@ -0,0 +1,46 @@
|
||||
# CLI Archive Command - Changes
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Spec Update Process
|
||||
|
||||
Before moving the change to archive, the command SHALL apply delta changes to main specs to reflect the deployed reality.
|
||||
|
||||
#### Scenario: Applying delta changes
|
||||
|
||||
- **WHEN** archiving a change with delta-based specs
|
||||
- **THEN** parse and apply delta changes as defined in openspec-conventions
|
||||
- **AND** validate all operations before applying
|
||||
|
||||
#### Scenario: Validating delta changes
|
||||
|
||||
- **WHEN** processing delta changes
|
||||
- **THEN** perform validations as specified in openspec-conventions
|
||||
- **AND** if validation fails, show specific errors and abort
|
||||
|
||||
#### Scenario: Conflict detection
|
||||
|
||||
- **WHEN** applying deltas would create duplicate requirement headers
|
||||
- **THEN** abort with error message showing the conflict
|
||||
- **AND** suggest manual resolution
|
||||
|
||||
### Requirement: Display Output
|
||||
|
||||
The command SHALL provide clear feedback about delta operations.
|
||||
|
||||
#### Scenario: Showing delta application
|
||||
|
||||
- **WHEN** applying delta changes
|
||||
- **THEN** display for each spec:
|
||||
- Number of requirements added
|
||||
- Number of requirements modified
|
||||
- Number of requirements removed
|
||||
- Number of requirements renamed
|
||||
- **AND** use standard output symbols (+ ~ - →) as defined in openspec-conventions:
|
||||
```
|
||||
Applying changes to specs/user-auth/spec.md:
|
||||
+ 2 added
|
||||
~ 3 modified
|
||||
- 1 removed
|
||||
→ 1 renamed
|
||||
```
|
||||
@@ -0,0 +1,43 @@
|
||||
# CLI Diff Command - Changes
|
||||
|
||||
## REMOVED Requirements
|
||||
|
||||
### Requirement: Display Format
|
||||
|
||||
The diff command SHALL display unified diff output in text format.
|
||||
|
||||
**Reason for removal**: The standard unified diff format is replaced by requirement-level side-by-side comparison that better shows semantic changes rather than line-by-line text differences.
|
||||
|
||||
#### Scenario: Unified diff output (deprecated)
|
||||
|
||||
- **WHEN** running `openspec diff <change>`
|
||||
- **THEN** show a unified text diff of files
|
||||
- **AND** include `+`/`-` prefixed lines representing additions and removals
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Diff Output
|
||||
|
||||
The command SHALL show a requirement-level comparison displaying only changed requirements.
|
||||
|
||||
#### Scenario: Side-by-side comparison of changes
|
||||
|
||||
- **WHEN** running `openspec diff <change>`
|
||||
- **THEN** display only requirements that have changed
|
||||
- **AND** show them in a side-by-side format that:
|
||||
- Clearly shows the current version on the left
|
||||
- Shows the future version on the right
|
||||
- Indicates new requirements (not in current)
|
||||
- Indicates removed requirements (not in future)
|
||||
- Aligns modified requirements for easy comparison
|
||||
|
||||
### Requirement: Validation
|
||||
|
||||
The command SHALL validate that changes can be applied successfully.
|
||||
|
||||
#### Scenario: Invalid delta references
|
||||
|
||||
- **WHEN** delta references non-existent requirement
|
||||
- **THEN** show error message with specific requirement
|
||||
- **AND** continue showing other valid changes
|
||||
- **AND** clearly mark failed changes in the output
|
||||
@@ -0,0 +1,117 @@
|
||||
# OpenSpec Conventions - Changes
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Header-Based Requirement Identification
|
||||
|
||||
Requirement headers SHALL serve as unique identifiers for programmatic matching between current specs and proposed changes.
|
||||
|
||||
#### Scenario: Matching requirements programmatically
|
||||
|
||||
- **WHEN** processing delta changes
|
||||
- **THEN** use the `### Requirement: [Name]` header as the unique identifier
|
||||
- **AND** match using normalized headers: `normalize(header) = trim(header)`
|
||||
- **AND** compare headers with case-sensitive equality after normalization
|
||||
|
||||
#### Scenario: Handling requirement renames
|
||||
|
||||
- **WHEN** renaming a requirement
|
||||
- **THEN** use a special `## RENAMED Requirements` section
|
||||
- **AND** specify both old and new names explicitly:
|
||||
```markdown
|
||||
## RENAMED Requirements
|
||||
- FROM: `### Requirement: Old Name`
|
||||
- TO: `### Requirement: New Name`
|
||||
```
|
||||
- **AND** if content also changes, include under MODIFIED using the NEW header
|
||||
|
||||
#### Scenario: Validating header uniqueness
|
||||
|
||||
- **WHEN** creating or modifying requirements
|
||||
- **THEN** ensure no duplicate headers exist within a spec
|
||||
- **AND** validation tools SHALL flag duplicate headers as errors
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Change Storage Convention
|
||||
|
||||
Change proposals SHALL store only the additions, modifications, and removals to specifications, not complete future states.
|
||||
|
||||
#### Scenario: Creating change proposals with additions
|
||||
|
||||
- **WHEN** creating a change proposal that adds new requirements
|
||||
- **THEN** include only the new requirements under `## ADDED Requirements`
|
||||
- **AND** each requirement SHALL include its complete content
|
||||
- **AND** use the standard structured format for requirements and scenarios
|
||||
|
||||
#### Scenario: Creating change proposals with modifications
|
||||
|
||||
- **WHEN** creating a change proposal that modifies existing requirements
|
||||
- **THEN** include the modified requirements under `## MODIFIED Requirements`
|
||||
- **AND** use the same header text as in the current spec (normalized)
|
||||
- **AND** include the complete modified requirement (not a diff)
|
||||
- **AND** optionally annotate what changed with inline comments like `← (was X)`
|
||||
|
||||
#### Scenario: Creating change proposals with removals
|
||||
|
||||
- **WHEN** creating a change proposal that removes requirements
|
||||
- **THEN** list them under `## REMOVED Requirements`
|
||||
- **AND** use the normalized header text for identification
|
||||
- **AND** include reason for removal
|
||||
- **AND** document any migration path if applicable
|
||||
|
||||
|
||||
The `changes/[name]/specs/` directory SHALL contain:
|
||||
- Delta files showing only what changes
|
||||
- Sections for ADDED, MODIFIED, REMOVED, and RENAMED requirements
|
||||
- Normalized header matching for requirement identification
|
||||
- Complete requirements using the structured format
|
||||
- Clear indication of change type for each requirement
|
||||
|
||||
#### Scenario: Using standard output symbols
|
||||
|
||||
- **WHEN** displaying delta operations in CLI output
|
||||
- **THEN** use these standard symbols:
|
||||
- `+` for ADDED (green)
|
||||
- `~` for MODIFIED (yellow)
|
||||
- `-` for REMOVED (red)
|
||||
- `→` for RENAMED (cyan)
|
||||
|
||||
### Requirement: Archive Process Enhancement
|
||||
|
||||
The archive process SHALL programmatically apply delta changes to current specifications using header-based matching.
|
||||
|
||||
#### Scenario: Archiving changes with deltas
|
||||
|
||||
- **WHEN** archiving a completed change
|
||||
- **THEN** the archive command SHALL:
|
||||
1. Parse RENAMED sections first and apply renames
|
||||
2. Parse REMOVED sections and remove by normalized header match
|
||||
3. Parse MODIFIED sections and replace by normalized header match (using new names if renamed)
|
||||
4. Parse ADDED sections and append new requirements
|
||||
- **AND** validate that all MODIFIED/REMOVED headers exist in current spec
|
||||
- **AND** validate that ADDED headers don't already exist
|
||||
- **AND** generate the updated spec in the main specs/ directory
|
||||
|
||||
#### Scenario: Handling conflicts during archive
|
||||
|
||||
- **WHEN** delta changes conflict with current spec state
|
||||
- **THEN** the archive command SHALL report specific conflicts
|
||||
- **AND** require manual resolution before proceeding
|
||||
- **AND** provide clear guidance on resolving conflicts
|
||||
|
||||
## REMOVED Requirements
|
||||
|
||||
### Requirement: Future State Storage
|
||||
|
||||
The system SHALL no longer store complete future-state specifications in change proposals.
|
||||
|
||||
**Reason for removal**: Replaced by delta-based change storage which provides better review experience and clearer change tracking.
|
||||
|
||||
**Migration path**: All new changes must use delta format.
|
||||
|
||||
#### Scenario: Deprecate future state storage
|
||||
|
||||
- **WHEN** creating a new change proposal
|
||||
- **THEN** do not include full future-state specs
|
||||
- **AND** include only ADDED/MODIFIED/REMOVED/RENAMED requirements under the change's `specs/` directory
|
||||
@@ -0,0 +1,55 @@
|
||||
# Implementation Tasks
|
||||
|
||||
## 1. Update Conventions
|
||||
- [x] 1.1 Update openspec-conventions spec with delta-based approach
|
||||
- [x] 1.2 Add Header-Based Requirement Identification
|
||||
- [x] 1.3 Define ADDED/MODIFIED/REMOVED/RENAMED sections
|
||||
- [x] 1.4 Document standard output symbols (+ ~ - →)
|
||||
- [x] 1.5 Update openspec/README.md with delta-based conventions
|
||||
- [x] 1.6 Update examples to use delta format
|
||||
|
||||
## 2. Update Diff Command
|
||||
- [ ] 2.1 Update cli-diff spec with requirement-level comparison
|
||||
- [ ] 2.2 Parse specs into requirement-level structures
|
||||
- [ ] 2.3 Apply deltas to generate future state
|
||||
- [ ] 2.4 Implement side-by-side comparison view (changes only)
|
||||
- [ ] 2.5 Add tests for requirement-level comparison
|
||||
- [ ] 2.6 Add tests for side-by-side view formatting
|
||||
|
||||
## 3. Update Archive Command
|
||||
- [x] 3.1 Update cli-archive spec with delta processing behavior
|
||||
- [x] 3.2 Implement requirement-block extractor that preserves exact headers (`### Requirement: [Name]`) and captures full content (including scenarios)
|
||||
- [x] 3.3 Implement normalized header matching (trim-only, case-sensitive)
|
||||
- [x] 3.4 Parse delta sections (ADDED/MODIFIED/REMOVED/RENAMED)
|
||||
- [x] 3.5 New spec creation when target spec does not exist
|
||||
- [x] 3.5.1 Auto-generate minimal skeleton: `# [Spec Name] Specification`, `## Purpose` placeholder, `## Requirements`
|
||||
- [x] 3.5.2 Allow only ADDED operations for non-existent specs; abort if MODIFIED/REMOVED/RENAMED present
|
||||
- [x] 3.6 Apply changes in order: RENAMED → REMOVED → MODIFIED → ADDED
|
||||
- [x] 3.7 Validation and conflict checks
|
||||
- [x] 3.7.1 MODIFIED/REMOVED requirements exist (after applying rename mappings)
|
||||
- [x] 3.7.2 ADDED requirements don't already exist (consider post-rename state)
|
||||
- [x] 3.7.3 RENAMED FROM headers exist; TO headers don't (including collisions with ADDED)
|
||||
- [x] 3.7.4 No duplicate headers within specs after all operations
|
||||
- [x] 3.7.5 Detect cross-section conflicts (e.g., same requirement in MODIFIED and REMOVED)
|
||||
- [x] 3.7.6 When a rename exists, require MODIFIED to reference the NEW header
|
||||
- [x] 3.8 Atomic updates
|
||||
- [x] 3.8.1 Validate all deltas first; stage updates in-memory per spec
|
||||
- [x] 3.8.2 Single write per spec; abort entire archive on any validation failure (no partial writes)
|
||||
- [x] 3.9 Output and error messaging
|
||||
- [x] 3.9.1 Display per-spec operation counts with symbols: `+` added, `~` modified, `-` removed, `→` renamed
|
||||
- [x] 3.9.2 Optionally display an aggregated totals line across all specs
|
||||
- [x] 3.9.3 Standardize error message format: `[spec] [operation] failed for header "### Requirement: X" — reason`; end with `Aborted. No files were changed.` on failure
|
||||
- [x] 3.10 Idempotency behavior (v1): abort on precondition failures (e.g., ADDED already exists); do not implement no-op detection
|
||||
- [x] 3.11 Tests
|
||||
- [x] 3.11.1 Header normalization (trim-only) matching
|
||||
- [x] 3.11.2 Apply in correct order (RENAMED → REMOVED → MODIFIED → ADDED)
|
||||
- [x] 3.11.3 Validation edge cases (missing headers, duplicates, rename collisions, conflicting sections)
|
||||
- [x] 3.11.4 Rename + modify interplay (MODIFIED uses new header)
|
||||
- [x] 3.11.5 New spec creation via skeleton
|
||||
- [x] 3.11.6 Multi-spec mixed operations with independent validation and write
|
||||
|
||||
## Notes
|
||||
- Archive command is critical path - must work reliably
|
||||
- All new changes must use delta format
|
||||
- Header normalization: normalize(header) = trim(header)
|
||||
- Diff command shows only changed requirements in side-by-side comparison
|
||||
@@ -0,0 +1,86 @@
|
||||
# Technical Design
|
||||
|
||||
## Architecture Decisions
|
||||
|
||||
### Simplicity First
|
||||
- No version tracking - always update when commanded
|
||||
- Full replacement for OpenSpec-managed files only (e.g., `openspec/README.md`)
|
||||
- Marker-based updates for user-owned files (e.g., `CLAUDE.md`)
|
||||
- Templates bundled with package - no network required
|
||||
- Minimal error handling - only check prerequisites
|
||||
|
||||
### Template Strategy
|
||||
- Use existing template utilities
|
||||
- `readmeTemplate` from `src/core/templates/readme-template.ts` for `openspec/README.md`
|
||||
- `TemplateManager.getClaudeTemplate()` for `CLAUDE.md`
|
||||
- Directory name is fixed to `openspec` (from `OPENSPEC_DIR_NAME`)
|
||||
|
||||
### File Operations
|
||||
- Use async utilities for consistency
|
||||
- `FileSystemUtils.writeFile` for `openspec/README.md`
|
||||
- `FileSystemUtils.updateFileWithMarkers` for `CLAUDE.md`
|
||||
- No atomic operations needed - users have git
|
||||
- Check directory existence before proceeding
|
||||
|
||||
## Implementation
|
||||
|
||||
### Update Command (`src/core/update.ts`)
|
||||
```typescript
|
||||
export class UpdateCommand {
|
||||
async execute(projectPath: string): Promise<void> {
|
||||
const openspecDirName = OPENSPEC_DIR_NAME;
|
||||
const openspecPath = path.join(projectPath, openspecDirName);
|
||||
|
||||
// 1. Check openspec directory exists
|
||||
if (!await FileSystemUtils.directoryExists(openspecPath)) {
|
||||
throw new Error(`No OpenSpec directory found. Run 'openspec init' first.`);
|
||||
}
|
||||
|
||||
// 2. Update README.md (full replacement)
|
||||
const readmePath = path.join(openspecPath, 'README.md');
|
||||
await FileSystemUtils.writeFile(readmePath, readmeTemplate);
|
||||
|
||||
// 3. Update CLAUDE.md (marker-based)
|
||||
const claudePath = path.join(projectPath, 'CLAUDE.md');
|
||||
const claudeContent = TemplateManager.getClaudeTemplate();
|
||||
await FileSystemUtils.updateFileWithMarkers(
|
||||
claudePath,
|
||||
claudeContent,
|
||||
OPENSPEC_MARKERS.start,
|
||||
OPENSPEC_MARKERS.end
|
||||
);
|
||||
|
||||
// 4. Success message (ASCII-safe, checkmark optional by terminal)
|
||||
console.log('Updated OpenSpec instructions');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Why This Approach
|
||||
|
||||
### Benefits
|
||||
- **Dead simple**: ~40 lines of code total
|
||||
- **Fast**: No version checks, minimal parsing
|
||||
- **Predictable**: Same result every time; idempotent
|
||||
- **Maintainable**: Reuses existing utilities
|
||||
|
||||
### Trade-offs Accepted
|
||||
- No version tracking (unnecessary complexity)
|
||||
- Full overwrite only for OpenSpec-managed files
|
||||
- Marker-managed updates for user-owned files
|
||||
|
||||
## Error Handling
|
||||
|
||||
Only handle critical errors:
|
||||
- Missing `openspec` directory → throw error handled by CLI to present a friendly message
|
||||
- File write failures → let errors bubble up to CLI
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
Manual smoke tests are sufficient initially:
|
||||
1. Run `openspec init` in a test project
|
||||
2. Modify both files (including custom content around markers in `CLAUDE.md`)
|
||||
3. Run `openspec update`
|
||||
4. Verify `openspec/README.md` fully replaced; `CLAUDE.md` OpenSpec block updated without altering user content outside markers
|
||||
5. Run the command twice to verify idempotency and no duplicate markers
|
||||
6. Test with missing `openspec` directory (expect failure)
|
||||
@@ -0,0 +1,29 @@
|
||||
# Add Update Command
|
||||
|
||||
## Why
|
||||
|
||||
Users need a way to update their local OpenSpec instructions (README.md and CLAUDE.md) when the OpenSpec package releases new versions with improved AI agent instructions or structural conventions.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Add new `openspec update` CLI command that updates OpenSpec instructions
|
||||
- Replace `openspec/README.md` with the latest template
|
||||
- Safe because this file is fully OpenSpec-managed
|
||||
- Update only the OpenSpec-managed block in `CLAUDE.md` using markers
|
||||
- Preserve all user content outside markers
|
||||
- If `CLAUDE.md` is missing, create it with the managed block
|
||||
- Display success message after update (ASCII-safe): "Updated OpenSpec instructions"
|
||||
- A leading checkmark MAY be shown when the terminal supports it
|
||||
- Operation is idempotent (re-running yields identical results)
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected specs: `cli-update` (new capability)
|
||||
- Affected code:
|
||||
- `src/core/update.ts` (new command class, mirrors `InitCommand` placement)
|
||||
- `src/cli/index.ts` (register new command)
|
||||
- Uses existing templates via `TemplateManager` and `readmeTemplate`
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- No `.openspec/config.json` is introduced by this change. The default directory name `openspec` is used.
|
||||
@@ -0,0 +1,59 @@
|
||||
# Update Command Specification
|
||||
|
||||
## Purpose
|
||||
|
||||
As a developer using OpenSpec, I want to update the OpenSpec instructions in my project when new versions are released, so that I can benefit from improvements to AI agent instructions.
|
||||
|
||||
## Core Requirements
|
||||
|
||||
### Update Behavior
|
||||
|
||||
The update command SHALL update OpenSpec instruction files to the latest templates.
|
||||
|
||||
WHEN a user runs `openspec update` THEN the command SHALL:
|
||||
- Check if the `openspec` directory exists
|
||||
- Replace `openspec/README.md` with the latest template (complete replacement)
|
||||
- Update the OpenSpec-managed block in `CLAUDE.md` using markers
|
||||
- Preserve user content outside markers
|
||||
- Create `CLAUDE.md` if missing
|
||||
- Display ASCII-safe success message: "Updated OpenSpec instructions"
|
||||
|
||||
### Prerequisites
|
||||
|
||||
The command SHALL require:
|
||||
- An existing `openspec` directory (created by `openspec init`)
|
||||
|
||||
IF the `openspec` directory does not exist THEN:
|
||||
- Display error: "No OpenSpec directory found. Run 'openspec init' first."
|
||||
- Exit with code 1
|
||||
|
||||
### File Handling
|
||||
|
||||
The update command SHALL:
|
||||
- Completely replace `openspec/README.md` with the latest template
|
||||
- Update only the OpenSpec-managed block in `CLAUDE.md` using markers
|
||||
- Use the default directory name `openspec`
|
||||
- Be idempotent (repeated runs have no additional effect)
|
||||
|
||||
## Edge Cases
|
||||
|
||||
### File Permissions
|
||||
IF file write fails THEN let the error bubble up naturally with file path.
|
||||
|
||||
### Missing CLAUDE.md
|
||||
IF CLAUDE.md doesn't exist THEN create it with the template content.
|
||||
|
||||
### Custom Directory Name
|
||||
Not supported in this change. The default directory name `openspec` SHALL be used.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
Users SHALL be able to:
|
||||
- Update OpenSpec instructions with a single command
|
||||
- Get the latest AI agent instructions
|
||||
- See clear confirmation of the update
|
||||
|
||||
The update process SHALL be:
|
||||
- Simple and fast (no version checking)
|
||||
- Predictable (same result every time)
|
||||
- Self-contained (no network required)
|
||||
@@ -0,0 +1,20 @@
|
||||
# Implementation Tasks
|
||||
|
||||
## 1. Update Command Implementation
|
||||
- [x] 1.1 Create `src/core/update.ts` with `UpdateCommand` class
|
||||
- [x] 1.2 Check if `openspec` directory exists (use `FileSystemUtils.directoryExists`)
|
||||
- [x] 1.3 Write `readmeTemplate` to `openspec/README.md` using `FileSystemUtils.writeFile`
|
||||
- [x] 1.4 Update `CLAUDE.md` using markers via `FileSystemUtils.updateFileWithMarkers` and `TemplateManager.getClaudeTemplate()`
|
||||
- [x] 1.5 Display ASCII-safe success message: `Updated OpenSpec instructions`
|
||||
|
||||
## 2. CLI Integration
|
||||
- [x] 2.1 Register `update` command in `src/cli/index.ts`
|
||||
- [x] 2.2 Add command description: `Update OpenSpec instruction files`
|
||||
- [x] 2.3 Handle errors with `ora().fail(...)` and exit code 1 (missing `openspec` directory, file write errors)
|
||||
|
||||
## 3. Testing
|
||||
- [x] 3.1 Verify `openspec/README.md` is fully replaced with latest template
|
||||
- [x] 3.2 Verify `CLAUDE.md` OpenSpec block updates without altering user content outside markers
|
||||
- [x] 3.3 Verify idempotency (running twice yields identical files, no duplicate markers)
|
||||
- [x] 3.4 Verify error when `openspec` directory is missing with friendly message
|
||||
- [x] 3.5 Verify success message displays properly in ASCII-only terminals
|
||||
@@ -0,0 +1,20 @@
|
||||
# Add List Command to OpenSpec CLI
|
||||
|
||||
## Why
|
||||
|
||||
Developers need visibility into available changes and their status to understand the project's evolution and pending work.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Add `openspec list` command that displays all changes in the changes/ directory
|
||||
- Show each change name with task completion count (e.g., "add-auth: 3/5 tasks")
|
||||
- Display completion status indicator (✓ for fully complete, progress for partial)
|
||||
- Skip the archive/ subdirectory to focus on active changes
|
||||
- Simple table output for easy scanning
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected specs: New capability `cli-list` will be added
|
||||
- Affected code:
|
||||
- `src/cli/index.ts` - Add list command
|
||||
- `src/core/list.ts` - New file with directory scanning and task parsing (~60 lines)
|
||||
@@ -0,0 +1,69 @@
|
||||
# List Command Specification
|
||||
|
||||
## Purpose
|
||||
|
||||
The `openspec list` command SHALL provide developers with a quick overview of all active changes in the project, showing their names and task completion status.
|
||||
|
||||
## Behavior
|
||||
|
||||
### Command Execution
|
||||
|
||||
WHEN `openspec list` is executed
|
||||
THEN scan the `openspec/changes/` directory for change directories
|
||||
AND exclude the `archive/` subdirectory from results
|
||||
AND parse each change's `tasks.md` file to count task completion
|
||||
|
||||
### Task Counting
|
||||
|
||||
WHEN parsing a `tasks.md` file
|
||||
THEN count tasks matching these patterns:
|
||||
- Completed: Lines containing `- [x]`
|
||||
- Incomplete: Lines containing `- [ ]`
|
||||
AND calculate total tasks as the sum of completed and incomplete
|
||||
|
||||
### Output Format
|
||||
|
||||
WHEN displaying the list
|
||||
THEN show a table with columns:
|
||||
- Change name (directory name)
|
||||
- Task progress (e.g., "3/5 tasks" or "✓ Complete")
|
||||
- Status indicator:
|
||||
- `✓` for fully completed changes (all tasks done)
|
||||
- Progress fraction for partial completion
|
||||
|
||||
Example output:
|
||||
```
|
||||
Changes:
|
||||
add-auth-feature 3/5 tasks
|
||||
update-api-docs ✓ Complete
|
||||
fix-validation 0/2 tasks
|
||||
add-list-command 1/4 tasks
|
||||
```
|
||||
|
||||
### Empty State
|
||||
|
||||
WHEN no active changes exist (only archive/ or empty changes/)
|
||||
THEN display: "No active changes found."
|
||||
|
||||
### Error Handling
|
||||
|
||||
IF a change directory has no `tasks.md` file
|
||||
THEN display the change with "No tasks" status
|
||||
|
||||
IF `openspec/changes/` directory doesn't exist
|
||||
THEN display error: "No OpenSpec changes directory found. Run 'openspec init' first."
|
||||
AND exit with code 1
|
||||
|
||||
### Sorting
|
||||
|
||||
Changes SHALL be displayed in alphabetical order by change name for consistency.
|
||||
|
||||
## Why
|
||||
|
||||
Developers need a quick way to:
|
||||
- See what changes are in progress
|
||||
- Identify which changes are ready to archive
|
||||
- Understand the overall project evolution status
|
||||
- Get a bird's-eye view without opening multiple files
|
||||
|
||||
This command provides that visibility with minimal effort, following OpenSpec's philosophy of simplicity and clarity.
|
||||
@@ -0,0 +1,26 @@
|
||||
# Implementation Tasks
|
||||
|
||||
## 1. Core Implementation
|
||||
- [x] 1.1 Create `src/core/list.ts` with list logic
|
||||
- [x] 1.1.1 Implement directory scanning (exclude archive/)
|
||||
- [x] 1.1.2 Implement task counting from tasks.md files
|
||||
- [x] 1.1.3 Format output as simple table
|
||||
- [x] 1.2 Add list command to CLI in `src/cli/index.ts`
|
||||
- [x] 1.2.1 Register `openspec list` command
|
||||
- [x] 1.2.2 Connect to list.ts implementation
|
||||
|
||||
## 2. Error Handling
|
||||
- [x] 2.1 Handle missing openspec/changes/ directory
|
||||
- [x] 2.2 Handle changes without tasks.md files
|
||||
- [x] 2.3 Handle empty changes directory
|
||||
|
||||
## 3. Testing
|
||||
- [x] 3.1 Add tests for list functionality
|
||||
- [x] 3.1.1 Test with multiple changes
|
||||
- [x] 3.1.2 Test with completed changes
|
||||
- [x] 3.1.3 Test with no changes
|
||||
- [x] 3.1.4 Test error conditions
|
||||
|
||||
## 4. Documentation
|
||||
- [x] 4.1 Update CLI help text with list command
|
||||
- [x] 4.2 Add list command to README if applicable
|
||||
@@ -0,0 +1,104 @@
|
||||
# Technical Design for Init Command
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
The init command follows a modular architecture with clear separation of concerns:
|
||||
|
||||
```
|
||||
CLI Layer (src/cli/index.ts)
|
||||
↓
|
||||
Core Logic (src/core/init.ts)
|
||||
↓
|
||||
Templates (src/core/templates/)
|
||||
↓
|
||||
File System Utils (src/utils/file-system.ts)
|
||||
```
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
### 1. Template Management
|
||||
|
||||
**Decision**: Store templates as TypeScript modules rather than separate files
|
||||
**Rationale**:
|
||||
- Ensures templates are bundled with the compiled code
|
||||
- Allows for dynamic content insertion
|
||||
- Type-safe template handling
|
||||
- No need for complex file path resolution
|
||||
|
||||
### 2. Interactive vs Non-Interactive Mode
|
||||
|
||||
**Decision**: Support both interactive (default) and non-interactive modes
|
||||
**Rationale**:
|
||||
- Interactive mode for developer experience
|
||||
- Non-interactive for CI/CD and automation
|
||||
- Flags: `--yes` to accept defaults, `--no-input` for full automation
|
||||
|
||||
### 3. Directory Structure Creation
|
||||
|
||||
**Decision**: Create all directories upfront, then populate files
|
||||
**Rationale**:
|
||||
- Fail fast if permissions issues
|
||||
- Clear transaction boundary
|
||||
- Easier to clean up on failure
|
||||
|
||||
### 4. Error Handling Strategy
|
||||
|
||||
**Decision**: Implement rollback on failure
|
||||
**Rationale**:
|
||||
- Prevent partial installations
|
||||
- Clear error states
|
||||
- Better user experience
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### File System Operations
|
||||
|
||||
```typescript
|
||||
// Atomic directory creation with rollback
|
||||
interface InitTransaction {
|
||||
createdPaths: string[];
|
||||
rollback(): Promise<void>;
|
||||
commit(): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
### Template System
|
||||
|
||||
```typescript
|
||||
interface Template {
|
||||
path: string;
|
||||
content: string | ((context: ProjectContext) => string);
|
||||
}
|
||||
|
||||
interface ProjectContext {
|
||||
projectName: string;
|
||||
description: string;
|
||||
techStack: string[];
|
||||
conventions: string;
|
||||
}
|
||||
```
|
||||
|
||||
### CLI Command Structure
|
||||
|
||||
```bash
|
||||
openspec init [path] # Initialize in specified path (default: current directory)
|
||||
--yes # Accept all defaults
|
||||
--no-input # Skip all prompts
|
||||
--force # Overwrite existing OpenSpec directory
|
||||
--dry-run # Show what would be created
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Path Traversal**: Sanitize all user-provided paths
|
||||
2. **File Permissions**: Check write permissions before starting
|
||||
3. **Existing Files**: Never overwrite without explicit --force flag
|
||||
4. **Template Injection**: Sanitize user inputs in templates
|
||||
|
||||
## Future Extensibility
|
||||
|
||||
The design supports future enhancements:
|
||||
- Custom template sources
|
||||
- Project type presets (API, web app, library)
|
||||
- Migration from other documentation systems
|
||||
- Integration with version control systems
|
||||
@@ -0,0 +1,30 @@
|
||||
# Add Init Command for OpenSpec
|
||||
|
||||
## Why
|
||||
|
||||
Projects need a simple way to adopt OpenSpec conventions. Currently, users must manually create the directory structure and understand all the conventions, which creates friction for adoption. An init command would enable instant OpenSpec setup with proper structure and guidance.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Add `openspec init` CLI command that creates the complete OpenSpec directory structure
|
||||
- Generate template files (README.md with AI instructions, project.md template)
|
||||
- Interactive prompt to select which AI tools to configure (Claude Code initially, others marked as "coming soon")
|
||||
- Support for multiple AI coding assistants with extensible plugin architecture
|
||||
- Smart file updates using content markers to preserve existing configurations
|
||||
- Custom directory naming with `--dir` flag
|
||||
- Validation to prevent overwriting existing OpenSpec structures
|
||||
- Clear error messages with helpful guidance (e.g., suggesting 'openspec update' for existing structures)
|
||||
- Display actionable next steps after successful initialization
|
||||
|
||||
### Breaking Changes
|
||||
- None - this is a new feature
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected specs: None (new feature)
|
||||
- Affected code:
|
||||
- src/cli/index.ts (add init command)
|
||||
- src/core/init.ts (new - initialization logic)
|
||||
- src/core/templates/ (new - template files)
|
||||
- src/core/configurators/ (new - AI tool plugins)
|
||||
- src/utils/file-system.ts (new - file operations)
|
||||
@@ -0,0 +1,148 @@
|
||||
# CLI Init Specification
|
||||
|
||||
## Purpose
|
||||
|
||||
The `openspec init` command SHALL create a complete OpenSpec directory structure in any project, enabling immediate adoption of OpenSpec conventions with support for multiple AI coding assistants.
|
||||
|
||||
## Behavior
|
||||
|
||||
### Progress Indicators
|
||||
|
||||
WHEN executing initialization steps
|
||||
THEN validate environment silently in background (no output unless error)
|
||||
AND display progress with ora spinners:
|
||||
- Show spinner: "⠋ Creating OpenSpec structure..."
|
||||
- Then success: "✔ OpenSpec structure created"
|
||||
- Show spinner: "⠋ Configuring AI tools..."
|
||||
- Then success: "✔ AI tools configured"
|
||||
|
||||
### Directory Creation
|
||||
|
||||
WHEN `openspec init` is executed
|
||||
THEN create the following directory structure:
|
||||
```
|
||||
openspec/
|
||||
├── project.md
|
||||
├── README.md
|
||||
├── specs/
|
||||
└── changes/
|
||||
└── archive/
|
||||
```
|
||||
|
||||
### File Generation
|
||||
|
||||
The command SHALL generate:
|
||||
- `README.md` containing complete OpenSpec instructions for AI assistants
|
||||
- `project.md` with project context template
|
||||
|
||||
### AI Tool Configuration
|
||||
|
||||
WHEN run interactively
|
||||
THEN prompt user to select AI tools to configure:
|
||||
- Claude Code (updates/creates CLAUDE.md with OpenSpec markers)
|
||||
- Cursor (future)
|
||||
- Aider (future)
|
||||
|
||||
### AI Tool Configuration Details
|
||||
|
||||
WHEN Claude Code is selected
|
||||
THEN create or update `CLAUDE.md` in the project root directory (not inside openspec/)
|
||||
|
||||
WHEN CLAUDE.md does not exist
|
||||
THEN create new file with OpenSpec content wrapped in markers:
|
||||
```markdown
|
||||
<!-- OPENSPEC:START -->
|
||||
# OpenSpec Project
|
||||
|
||||
This document provides instructions for AI coding assistants on how to use OpenSpec conventions for spec-driven development. Follow these rules precisely when working on OpenSpec-enabled projects.
|
||||
|
||||
This project uses OpenSpec for spec-driven development. Specifications are the source of truth.
|
||||
|
||||
See @openspec/README.md for detailed conventions and guidelines.
|
||||
<!-- OPENSPEC:END -->
|
||||
```
|
||||
|
||||
WHEN CLAUDE.md already exists
|
||||
THEN preserve all existing content
|
||||
AND insert OpenSpec content at the beginning of the file using markers
|
||||
AND ensure markers don't duplicate if they already exist
|
||||
|
||||
The marker system SHALL:
|
||||
- Use `<!-- OPENSPEC:START -->` to mark the beginning of managed content
|
||||
- Use `<!-- OPENSPEC:END -->` to mark the end of managed content
|
||||
- Allow OpenSpec to update its content without affecting user customizations
|
||||
- Preserve all content outside the markers intact
|
||||
|
||||
WHY use markers:
|
||||
- Users may have existing CLAUDE.md instructions they want to keep
|
||||
- OpenSpec can update its instructions in future versions
|
||||
- Clear boundary between OpenSpec-managed and user-managed content
|
||||
|
||||
### Interactive Mode
|
||||
|
||||
WHEN run
|
||||
THEN prompt user with: "Which AI tool do you use?"
|
||||
AND show single-select menu with available tools:
|
||||
- Claude Code
|
||||
AND show disabled options as "coming soon" (not selectable):
|
||||
- Cursor (coming soon)
|
||||
- Aider (coming soon)
|
||||
- Continue (coming soon)
|
||||
|
||||
User navigation:
|
||||
- Use arrow keys to move between options
|
||||
- Press Enter to select the highlighted option
|
||||
|
||||
### Safety Checks
|
||||
|
||||
WHEN `openspec/` directory already exists
|
||||
THEN display error with ora fail indicator:
|
||||
"✖ Error: OpenSpec seems to already be initialized. Use 'openspec update' to update the structure."
|
||||
|
||||
WHEN checking initialization feasibility
|
||||
THEN verify write permissions in the target directory silently
|
||||
AND only display error if permissions are insufficient
|
||||
|
||||
### Success Output
|
||||
|
||||
WHEN initialization completes successfully
|
||||
THEN display actionable prompts for AI-driven workflow:
|
||||
```
|
||||
✔ OpenSpec initialized successfully!
|
||||
|
||||
Next steps - Copy these prompts to Claude:
|
||||
|
||||
────────────────────────────────────────────────────────────
|
||||
1. Populate your project context:
|
||||
"Please read openspec/project.md and help me fill it out
|
||||
with details about my project, tech stack, and conventions"
|
||||
|
||||
2. Create your first change proposal:
|
||||
"I want to add [YOUR FEATURE HERE]. Please create an
|
||||
OpenSpec change proposal for this feature"
|
||||
|
||||
3. Learn the OpenSpec workflow:
|
||||
"Please explain the OpenSpec workflow from openspec/README.md
|
||||
and how I should work with you on this project"
|
||||
────────────────────────────────────────────────────────────
|
||||
```
|
||||
|
||||
The prompts SHALL:
|
||||
- Be copy-pasteable for immediate use with AI tools
|
||||
- Guide users through the AI-driven workflow
|
||||
- Replace placeholder text ([YOUR FEATURE HERE]) with actual features
|
||||
|
||||
### Exit Codes
|
||||
|
||||
- 0: Success
|
||||
- 1: General error (including when OpenSpec directory already exists)
|
||||
- 2: Insufficient permissions (reserved for future use)
|
||||
- 3: User cancelled operation (reserved for future use)
|
||||
|
||||
## Why
|
||||
|
||||
Manual creation of OpenSpec structure is error-prone and creates adoption friction. A standardized init command ensures:
|
||||
- Consistent structure across all projects
|
||||
- Proper AI instruction files are always included
|
||||
- Quick onboarding for new projects
|
||||
- Clear conventions from the start
|
||||
@@ -0,0 +1,38 @@
|
||||
# Implementation Tasks for Init Command
|
||||
|
||||
## 1. Core Infrastructure
|
||||
- [x] 1.1 Create src/utils/file-system.ts with directory/file creation utilities
|
||||
- [x] 1.2 Create src/core/templates/index.ts for template management
|
||||
- [x] 1.3 Create src/core/init.ts with main initialization logic
|
||||
- [x] 1.4 Create src/core/config.ts for configuration management
|
||||
|
||||
## 2. Template Files
|
||||
- [x] 2.1 Create src/core/templates/readme-template.ts with OpenSpec README content
|
||||
- [x] 2.2 Create src/core/templates/project-template.ts with customizable project.md
|
||||
- [x] 2.3 Create src/core/templates/claude-template.ts for CLAUDE.md content with markers
|
||||
|
||||
## 3. AI Tool Configurators
|
||||
- [x] 3.1 Create src/core/configurators/base.ts with ToolConfigurator interface
|
||||
- [x] 3.2 Create src/core/configurators/claude.ts for Claude Code configuration
|
||||
- [x] 3.3 Create src/core/configurators/registry.ts for tool registration
|
||||
- [x] 3.4 Implement marker-based file updates for existing configurations
|
||||
|
||||
## 4. Init Command Implementation
|
||||
- [x] 4.1 Add init command to src/cli/index.ts using Commander
|
||||
- [x] 4.2 Implement AI tool selection with multi-select prompt (Claude Code available, others "coming soon") - requires at least one selection
|
||||
- [x] 4.3 Add validation for existing OpenSpec directories with helpful error message
|
||||
- [x] 4.4 Implement directory structure creation
|
||||
- [x] 4.5 Implement file generation with templates and markers
|
||||
|
||||
## 5. User Experience
|
||||
- [x] 5.1 Add colorful console output for better UX
|
||||
- [x] 5.2 Implement progress indicators (Step 1/3, 2/3, 3/3)
|
||||
- [x] 5.3 Add success message with actionable next steps (edit project.md, create first change)
|
||||
- [x] 5.4 Add error handling with helpful messages
|
||||
|
||||
## 6. Testing and Documentation
|
||||
- [x] 6.1 Add unit tests for file system utilities
|
||||
- [x] 6.2 Add unit tests for marker-based file updates
|
||||
- [x] 6.3 Add integration tests for init command
|
||||
- [x] 6.4 Update package.json with proper bin configuration
|
||||
- [x] 6.5 Test the built CLI command end-to-end
|
||||
@@ -0,0 +1,24 @@
|
||||
# Adopt Future State Storage for OpenSpec Changes
|
||||
|
||||
## Why
|
||||
|
||||
The current approach of storing spec changes as diff files (`.spec.md.diff`) creates friction for both humans and AI. Diff syntax with `+` and `-` prefixes makes specs hard to read, AI tools struggle with the format when understanding future state, and GitHub can't show nice comparisons between current and proposed specs in different folders.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Change from storing diffs (`patches/[capability]/spec.md.diff`) to storing complete future state (`specs/[capability]/spec.md`)
|
||||
- Update all documentation to reflect new storage format
|
||||
- Migrate existing `add-init-command` change to new format
|
||||
- Add new `openspec-conventions` capability to document these conventions
|
||||
|
||||
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected specs: New `openspec-conventions` capability
|
||||
- Affected code:
|
||||
- openspec/README.md (lines 85-108)
|
||||
- docs/PRD.md (lines 376-382, 778-783)
|
||||
- docs/openspec-walkthrough.md (lines 58-62, 112-126)
|
||||
- openspec/changes/add-init-command/ (migration needed)
|
||||
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
# OpenSpec Conventions Specification
|
||||
|
||||
## Purpose
|
||||
|
||||
OpenSpec conventions SHALL define how system capabilities are documented, how changes are proposed and tracked, and how specifications evolve over time. This meta-specification serves as the source of truth for OpenSpec's own conventions.
|
||||
|
||||
## Core Principles
|
||||
|
||||
The system SHALL follow these principles:
|
||||
- Specs reflect what IS currently built and deployed
|
||||
- Changes contain proposals for what SHOULD be changed
|
||||
- AI drives the documentation process
|
||||
- Specs are living documentation kept in sync with deployed code
|
||||
|
||||
## Directory Structure
|
||||
|
||||
WHEN an OpenSpec project is initialized
|
||||
THEN it SHALL have this structure:
|
||||
```
|
||||
openspec/
|
||||
├── project.md # Project-specific context
|
||||
├── README.md # AI assistant instructions
|
||||
├── specs/ # Current deployed capabilities
|
||||
│ └── [capability]/ # Single, focused capability
|
||||
│ ├── spec.md # WHAT and WHY
|
||||
│ └── design.md # HOW (optional, for established patterns)
|
||||
└── changes/ # Proposed changes
|
||||
├── [change-name]/ # Descriptive change identifier
|
||||
│ ├── proposal.md # Why, what, and impact
|
||||
│ ├── tasks.md # Implementation checklist
|
||||
│ ├── design.md # Technical decisions (optional)
|
||||
│ └── specs/ # Complete future state
|
||||
│ └── [capability]/
|
||||
│ └── spec.md # Clean markdown (no diff syntax)
|
||||
└── archive/ # Completed changes
|
||||
└── YYYY-MM-DD-[name]/
|
||||
```
|
||||
|
||||
## Change Storage Convention
|
||||
|
||||
### Future State Storage
|
||||
|
||||
WHEN creating a change proposal
|
||||
THEN store the complete future state of affected specs
|
||||
AND use clean markdown without diff syntax
|
||||
|
||||
The `changes/[name]/specs/` directory SHALL contain:
|
||||
- Complete spec files as they will exist after the change
|
||||
- Clean markdown without `+` or `-` prefixes
|
||||
- All formatting and structure of the final intended state
|
||||
|
||||
### Proposal Format
|
||||
|
||||
WHEN documenting what changes
|
||||
THEN the proposal SHALL explicitly describe each change:
|
||||
|
||||
```markdown
|
||||
**[Section or Behavior Name]**
|
||||
- From: [current state/requirement]
|
||||
- To: [future state/requirement]
|
||||
- Reason: [why this change is needed]
|
||||
- Impact: [breaking/non-breaking, who's affected]
|
||||
```
|
||||
|
||||
This explicit format compensates for not having inline diffs and ensures reviewers understand exactly what will change.
|
||||
|
||||
## Change Lifecycle
|
||||
|
||||
The change process SHALL follow these states:
|
||||
|
||||
1. **Propose**: AI creates change with future state specs and explicit proposal
|
||||
2. **Review**: Humans review proposal and future state
|
||||
3. **Approve**: Change is approved for implementation
|
||||
4. **Implement**: Follow tasks.md checklist (can span multiple PRs)
|
||||
5. **Deploy**: Changes are deployed to production
|
||||
6. **Update**: Specs in `specs/` are updated to match deployed reality
|
||||
7. **Archive**: Change is moved to `archive/YYYY-MM-DD-[name]/`
|
||||
|
||||
## Viewing Changes
|
||||
|
||||
WHEN reviewing proposed changes
|
||||
THEN reviewers can compare using:
|
||||
- GitHub PR diff view when changes are committed
|
||||
- Command line: `diff -u specs/[capability]/spec.md changes/[name]/specs/[capability]/spec.md`
|
||||
- Any visual diff tool comparing current vs future state
|
||||
|
||||
The system relies on tools to generate diffs rather than storing them.
|
||||
|
||||
## Capability Naming
|
||||
|
||||
Capabilities SHALL use:
|
||||
- Verb-noun patterns (e.g., `user-auth`, `payment-capture`)
|
||||
- Hyphenated lowercase names
|
||||
- Singular focus (one responsibility per capability)
|
||||
- No nesting (flat structure under `specs/`)
|
||||
|
||||
## When Changes Require Proposals
|
||||
|
||||
A proposal SHALL be created for:
|
||||
- New features or capabilities
|
||||
- Breaking changes to existing behavior
|
||||
- Architecture or pattern changes
|
||||
- Performance optimizations that change behavior
|
||||
- Security updates affecting access patterns
|
||||
|
||||
A proposal is NOT required for:
|
||||
- Bug fixes restoring intended behavior
|
||||
- Typos or formatting fixes
|
||||
- Non-breaking dependency updates
|
||||
- Adding tests for existing behavior
|
||||
- Documentation clarifications
|
||||
|
||||
## Why This Approach
|
||||
|
||||
Clean future state storage provides:
|
||||
- **Readability**: No diff syntax pollution
|
||||
- **AI-compatibility**: Standard markdown that AI tools understand
|
||||
- **Simplicity**: No special parsing or processing needed
|
||||
- **Tool-agnostic**: Any diff tool can show changes
|
||||
- **Clear intent**: Explicit proposals document reasoning
|
||||
@@ -0,0 +1,38 @@
|
||||
# Implementation Tasks
|
||||
|
||||
## 1. Update Core Documentation
|
||||
- [x] 1.1 Update openspec/README.md section on "Creating a Change Proposal"
|
||||
- [x] Replace `patches/` with `specs/` in directory structure
|
||||
- [x] Update step 3 to show storing complete future state
|
||||
- [x] Remove diff syntax instructions (+/- prefixes)
|
||||
|
||||
## 2. Migrate Existing Change
|
||||
- [x] 2.1 Convert add-init-command change to new format
|
||||
- [x] Create `specs/cli-init/spec.md` with clean content (no diff markers)
|
||||
- [x] Delete old `patches/` directory
|
||||
- [x] 2.2 Test that the migrated change is clear and reviewable
|
||||
|
||||
## 3. Update Documentation Examples
|
||||
- [x] 3.1 Update docs/PRD.md
|
||||
- [x] Fix directory structure examples (lines 376-382)
|
||||
- [x] Update archive examples (lines 778-783)
|
||||
- [x] Ensure consistency throughout
|
||||
- [x] 3.2 Update docs/openspec-walkthrough.md
|
||||
- [x] Replace diff examples with future state examples
|
||||
- [x] Ensure the walkthrough reflects new approach
|
||||
|
||||
## 4. Create New Spec
|
||||
- [x] 4.1 Finalize openspec-conventions spec in main specs/ directory
|
||||
- [x] Document the future state storage approach
|
||||
- [x] Include examples of good proposals
|
||||
- [x] Make it the source of truth for conventions
|
||||
|
||||
## 5. Validation
|
||||
- [x] 5.1 Verify all documentation is consistent
|
||||
- [x] 5.2 Test creating a new change with the new approach
|
||||
- [x] 5.3 Ensure GitHub PR view shows diffs clearly
|
||||
|
||||
## 6. Deployment
|
||||
- [x] 6.1 Get approval for this change
|
||||
- [x] 6.2 Implement all tasks above
|
||||
- [x] 6.3 After deployment, archive this change with completion date
|
||||
@@ -0,0 +1,13 @@
|
||||
# Add Complexity Management Guidelines
|
||||
|
||||
## Why
|
||||
OpenSpec currently lacks guidance on managing complexity, leading to over-engineered solutions when simple ones suffice.
|
||||
|
||||
## What Changes
|
||||
- Add "Start Simple" section to openspec/README.md with default minimalism rules
|
||||
- Add complexity triggers to help identify when complexity is justified
|
||||
- Enhance AI assistant instructions in CLAUDE.md to bias toward simplicity
|
||||
|
||||
## Impact
|
||||
- Affected specs: None (documentation only)
|
||||
- Affected code: openspec/README.md, CLAUDE.md
|
||||
+472
@@ -0,0 +1,472 @@
|
||||
# OpenSpec Instructions
|
||||
|
||||
This document provides instructions for AI coding assistants on how to use OpenSpec conventions for spec-driven development. Follow these rules precisely when working on OpenSpec-enabled projects.
|
||||
|
||||
## Core Principle
|
||||
|
||||
OpenSpec is an AI-native system for change-driven development where:
|
||||
- **Specs** (`specs/`) reflect what IS currently built and deployed
|
||||
- **Changes** (`changes/`) contain proposals for what SHOULD be changed
|
||||
- **AI drives the process** - You generate proposals, humans review and approve
|
||||
- **Specs are living documentation** - Always kept in sync with deployed code
|
||||
|
||||
## Start Simple
|
||||
|
||||
**Default to minimal implementations:**
|
||||
- New features should be <100 lines of code initially
|
||||
- Use the simplest solution that works
|
||||
- Avoid premature optimization (no caching, parallelization, or complex patterns without proven need)
|
||||
- Choose boring technology over cutting-edge solutions
|
||||
|
||||
**Complexity triggers** - Only add complexity when you have:
|
||||
- **Performance data** showing current solution is too slow
|
||||
- **Scale requirements** with specific numbers (>1000 users, >100MB data)
|
||||
- **Multiple use cases** requiring the same abstraction
|
||||
- **Regulatory compliance** mandating specific patterns
|
||||
- **Security threats** that simple solutions cannot address
|
||||
|
||||
When triggered, document the specific justification in your change proposal.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
openspec/
|
||||
├── project.md # Project-specific context (tech stack, conventions)
|
||||
├── README.md # This file - OpenSpec instructions
|
||||
├── specs/ # Current truth - what IS built
|
||||
│ ├── [capability]/ # Single, focused capability
|
||||
│ │ ├── spec.md # WHAT the capability does and WHY
|
||||
│ │ └── design.md # HOW it's built (established patterns)
|
||||
│ └── ...
|
||||
├── changes/ # Proposed changes - what we're CHANGING
|
||||
│ ├── [change-name]/
|
||||
│ │ ├── proposal.md # Why, what, impact (consolidated)
|
||||
│ │ ├── tasks.md # Implementation checklist
|
||||
│ │ ├── design.md # Technical decisions (optional, for complex changes)
|
||||
│ │ └── specs/ # Future state of affected specs
|
||||
│ │ └── [capability]/
|
||||
│ │ └── spec.md # Clean markdown (no diff syntax)
|
||||
│ └── archive/ # Completed changes (dated)
|
||||
```
|
||||
|
||||
### Capability Organization
|
||||
|
||||
**Use capabilities, not features** - Each directory under `specs/` represents a single, focused responsibility:
|
||||
- **Verb-noun naming**: `user-auth`, `payment-capture`, `order-checkout`
|
||||
- **10-minute rule**: Each capability should be understandable in <10 minutes
|
||||
- **Single purpose**: If it needs "AND" to describe it, split it
|
||||
|
||||
Examples:
|
||||
```
|
||||
✅ GOOD: user-auth, user-sessions, payment-capture, payment-refunds
|
||||
❌ BAD: users, payments, core, misc
|
||||
```
|
||||
|
||||
## Key Behavioral Rules
|
||||
|
||||
### 1. Always Start by Reading
|
||||
|
||||
Before any task:
|
||||
1. **Read relevant specs** in `specs/[capability]/spec.md` to understand current state
|
||||
2. **Check pending changes** in `changes/` directory for potential conflicts
|
||||
3. **Read project.md** for project-specific conventions
|
||||
|
||||
### 2. When to Create Change Proposals
|
||||
|
||||
**ALWAYS create a change proposal for:**
|
||||
- New features or functionality
|
||||
- Breaking changes (API changes, schema updates)
|
||||
- Architecture changes or new patterns
|
||||
- Performance optimizations that change behavior
|
||||
- Security updates affecting auth/access patterns
|
||||
- Any change requiring multiple steps or affecting multiple systems
|
||||
|
||||
**SKIP proposals for:**
|
||||
- Bug fixes that restore intended behavior
|
||||
- Typos, formatting, or comment updates
|
||||
- Dependency updates (unless breaking)
|
||||
- Configuration or environment variable changes
|
||||
- Adding tests for existing behavior
|
||||
- Documentation fixes
|
||||
|
||||
**Complexity assessment:**
|
||||
- If your solution requires >100 lines of new code, justify the complexity
|
||||
- If adding dependencies, frameworks, or architectural patterns, document why simpler alternatives won't work
|
||||
- Default to single-file implementations until proven insufficient
|
||||
|
||||
### 3. Creating a Change Proposal
|
||||
|
||||
When a user requests a significant change:
|
||||
|
||||
```bash
|
||||
# 1. Create the change directory
|
||||
openspec/changes/[descriptive-name]/
|
||||
|
||||
# 2. Generate proposal.md with all context
|
||||
## Why
|
||||
[1-2 sentences on the problem/opportunity]
|
||||
|
||||
## What Changes
|
||||
[Bullet list of changes, including breaking changes]
|
||||
|
||||
## Impact
|
||||
- Affected specs: [list capabilities that will change]
|
||||
- Affected code: [list key files/systems]
|
||||
|
||||
# 3. Create future state specs for ALL affected capabilities
|
||||
# - Store complete spec files as they will exist after the change
|
||||
# - Use clean markdown without diff syntax (+/- prefixes)
|
||||
# - Include all formatting and structure of the final intended state
|
||||
specs/
|
||||
└── [capability]/
|
||||
└── spec.md
|
||||
|
||||
# 4. Create tasks.md with implementation steps
|
||||
## 1. [Task Group]
|
||||
- [ ] 1.1 [Specific task]
|
||||
- [ ] 1.2 [Specific task]
|
||||
|
||||
# 5. For complex changes, add design.md
|
||||
[Technical decisions and trade-offs]
|
||||
```
|
||||
|
||||
### 4. The Change Lifecycle
|
||||
|
||||
1. **Propose** → Create change directory with all documentation
|
||||
2. **Review** → User reviews and approves the proposal
|
||||
3. **Implement** → Follow the approved tasks.md (can be multiple PRs)
|
||||
4. **Deploy** → User confirms deployment
|
||||
5. **Update Specs** → Sync specs/ with new reality (IF the change affects system capabilities)
|
||||
6. **Archive** → Move to `changes/archive/YYYY-MM-DD-[name]/`
|
||||
|
||||
### 5. Implementing Changes
|
||||
|
||||
When implementing an approved change:
|
||||
1. Follow the tasks.md checklist exactly
|
||||
2. **Mark completed tasks** in tasks.md as you finish them (e.g., `- [x] 1.1 Task completed`)
|
||||
3. Ensure code matches the proposed behavior
|
||||
4. Update any affected tests
|
||||
5. **Keep change in `changes/` directory** - do NOT archive in implementation PR
|
||||
|
||||
**Multiple Implementation PRs:**
|
||||
- Changes can be implemented across multiple PRs
|
||||
- Each PR should update tasks.md to mark what was completed
|
||||
- Different developers can work on different task groups
|
||||
- Example: PR #1 completes tasks 1.1-1.3, PR #2 completes tasks 2.1-2.4
|
||||
|
||||
### 6. Updating Specs and Archiving After Deployment
|
||||
|
||||
**Create a separate PR after deployment** that:
|
||||
1. Moves change to `changes/archive/YYYY-MM-DD-[name]/`
|
||||
2. Updates relevant files in `specs/` to reflect new reality (if needed)
|
||||
3. If design.md exists, incorporates proven patterns into `specs/[capability]/design.md`
|
||||
|
||||
This ensures changes are only archived when truly complete and deployed.
|
||||
|
||||
### 7. Types of Changes That Don't Require Specs
|
||||
|
||||
Some changes only affect development infrastructure and don't need specs:
|
||||
- Initial project setup (package.json, tsconfig.json, etc.)
|
||||
- Development tooling changes (linters, formatters, build tools)
|
||||
- CI/CD configuration
|
||||
- Development dependencies
|
||||
|
||||
For these changes:
|
||||
1. Implement → Deploy → Mark tasks complete → Archive
|
||||
2. Skip the "Update Specs" step entirely
|
||||
|
||||
### What Deserves a Spec?
|
||||
|
||||
Ask yourself:
|
||||
- Is this a system capability that users or other systems interact with?
|
||||
- Does it have ongoing behavior that needs documentation?
|
||||
- Would a new developer need to understand this to work with the system?
|
||||
|
||||
If NO to all → No spec needed (likely just tooling/infrastructure)
|
||||
|
||||
## Understanding Specs vs Code
|
||||
|
||||
### Specs Document WHAT and WHY
|
||||
```markdown
|
||||
# Authentication Spec
|
||||
|
||||
Users SHALL authenticate with email and password.
|
||||
|
||||
WHEN credentials are valid THEN issue JWT token.
|
||||
WHEN credentials are invalid THEN return generic error.
|
||||
|
||||
WHY: Prevent user enumeration attacks.
|
||||
```
|
||||
|
||||
### Code Documents HOW
|
||||
```javascript
|
||||
// Implementation details
|
||||
const user = await db.users.findOne({ email });
|
||||
const valid = await bcrypt.compare(password, user.hashedPassword);
|
||||
```
|
||||
|
||||
**Key Distinction**: Specs capture intent, constraints, and decisions that aren't obvious from code.
|
||||
|
||||
## Common Scenarios
|
||||
|
||||
### New Feature Request
|
||||
```
|
||||
User: "Add password reset functionality"
|
||||
|
||||
You should:
|
||||
1. Read specs/user-auth/spec.md
|
||||
2. Check changes/ for pending auth changes
|
||||
3. Create changes/add-password-reset/ with proposal
|
||||
4. Wait for approval before implementing
|
||||
```
|
||||
|
||||
### Bug Fix
|
||||
```
|
||||
User: "Getting null pointer error when bio is empty"
|
||||
|
||||
You should:
|
||||
1. Check if spec says bios are optional
|
||||
2. If yes → Fix directly (it's a bug)
|
||||
3. If no → Create change proposal (it's a behavior change)
|
||||
```
|
||||
|
||||
### Infrastructure Setup
|
||||
```
|
||||
User: "Initialize TypeScript project"
|
||||
|
||||
You should:
|
||||
1. Create change proposal for TypeScript setup
|
||||
2. Implement configuration files (PR #1)
|
||||
3. Mark tasks complete in tasks.md
|
||||
4. After deployment, create separate PR to archive
|
||||
(no specs update needed - this is tooling, not a capability)
|
||||
```
|
||||
|
||||
## Summary Workflow
|
||||
|
||||
1. **Receive request** → Determine if it needs a change proposal
|
||||
2. **Read current state** → Check specs and pending changes
|
||||
3. **Create proposal** → Generate complete change documentation
|
||||
4. **Get approval** → User reviews the proposal
|
||||
5. **Implement** → Follow approved tasks, mark completed items in tasks.md
|
||||
6. **Deploy** → User deploys the implementation
|
||||
7. **Archive PR** → Create separate PR to:
|
||||
- Move change to archive
|
||||
- Update specs if needed
|
||||
- Mark change as complete
|
||||
|
||||
## PR Workflow Examples
|
||||
|
||||
### Single Developer, Simple Change
|
||||
```
|
||||
PR #1: Implementation
|
||||
- Implement all tasks
|
||||
- Update tasks.md marking items complete
|
||||
- Get merged and deployed
|
||||
|
||||
PR #2: Archive (after deployment)
|
||||
- Move changes/feature-x/ → changes/archive/2025-01-15-feature-x/
|
||||
- Update specs if needed
|
||||
```
|
||||
|
||||
### Multiple Developers, Complex Change
|
||||
```
|
||||
PR #1: Alice implements auth components
|
||||
- Complete tasks 1.1, 1.2, 1.3
|
||||
- Update tasks.md marking these complete
|
||||
|
||||
PR #2: Bob implements UI components
|
||||
- Complete tasks 2.1, 2.2
|
||||
- Update tasks.md marking these complete
|
||||
|
||||
PR #3: Alice fixes integration issues
|
||||
- Complete remaining task 1.4
|
||||
- Update tasks.md
|
||||
|
||||
[Deploy all changes]
|
||||
|
||||
PR #4: Archive
|
||||
- Move to archive with deployment date
|
||||
- Update specs to reflect new auth flow
|
||||
```
|
||||
|
||||
### Key Rules
|
||||
- **Never archive in implementation PRs** - changes aren't done until deployed
|
||||
- **Always update tasks.md** - shows accurate progress
|
||||
- **One archive PR per change** - clear completion boundary
|
||||
- **Archive PR includes spec updates** - keeps specs current
|
||||
|
||||
## Capability Organization Best Practices
|
||||
|
||||
### Naming Capabilities
|
||||
- Use **verb-noun** patterns: `user-auth`, `payment-capture`, `order-checkout`
|
||||
- Be specific: `payment-capture` not just `payments`
|
||||
- Keep flat: Avoid nesting capabilities within capabilities
|
||||
- Singular focus: If you need "AND" to describe it, split it
|
||||
|
||||
### When to Split Capabilities
|
||||
Split when you have:
|
||||
- Multiple unrelated API endpoints
|
||||
- Different user personas or actors
|
||||
- Separate deployment considerations
|
||||
- Independent evolution paths
|
||||
|
||||
#### Capability Boundary Guidelines
|
||||
- Would you import these separately? → Separate capabilities
|
||||
- Different deployment cadence? → Separate capabilities
|
||||
- Different teams own them? → Separate capabilities
|
||||
- Shared data models are OK, shared business logic means combine
|
||||
|
||||
Examples:
|
||||
- user-auth (login/logout) vs user-sessions (token management) → SEPARATE
|
||||
- payment-capture vs payment-refunds → SEPARATE (different workflows)
|
||||
- user-profile vs user-settings → COMBINE (same data model, same owner)
|
||||
|
||||
### Cross-Cutting Concerns
|
||||
For system-wide policies (rate limiting, error handling, security), document them in:
|
||||
- `project.md` for project-wide conventions
|
||||
- Within relevant capability specs where they apply
|
||||
- Or create a dedicated capability if complex enough (e.g., `api-rate-limiting/`)
|
||||
|
||||
### Examples of Well-Organized Capabilities
|
||||
```
|
||||
specs/
|
||||
├── user-auth/ # Login, logout, password reset
|
||||
├── user-sessions/ # Token management, refresh
|
||||
├── user-profile/ # Profile CRUD operations
|
||||
├── payment-capture/ # Processing payments
|
||||
├── payment-refunds/ # Handling refunds
|
||||
└── order-checkout/ # Checkout workflow
|
||||
```
|
||||
|
||||
For detailed guidance, see the [Capability Organization Guide](../docs/capability-organization.md).
|
||||
|
||||
## Common Scenarios and Clarifications
|
||||
|
||||
### Decision Ambiguity: Bug vs Behavior Change
|
||||
|
||||
When specs are missing or ambiguous:
|
||||
- If NO spec exists → Treat current code behavior as implicit spec, require proposal
|
||||
- If spec is VAGUE → Require proposal to clarify spec alongside fix
|
||||
- If code and spec DISAGREE → Spec is truth, code is buggy (fix without proposal)
|
||||
- If unsure → Default to creating a proposal (safer option)
|
||||
|
||||
Example:
|
||||
```
|
||||
User: "The API returns 404 for missing users but should return 400"
|
||||
AI: Is this a bug (spec says 400) or behavior change (spec says 404)?
|
||||
```
|
||||
|
||||
### When You Don't Know the Scope
|
||||
It's OK to explore first! Tell the user you need to investigate, then create an informed proposal.
|
||||
|
||||
### Exploration Phase (When Needed)
|
||||
|
||||
BEFORE creating proposal, you may need exploration when:
|
||||
- User request is vague or high-level
|
||||
- Multiple implementation approaches exist
|
||||
- Scope is unclear without seeing code
|
||||
|
||||
Exploration checklist:
|
||||
1. Tell user you need to explore first
|
||||
2. Use Grep/Read to understand current state
|
||||
3. Create initial proposal based on findings
|
||||
4. Refine with user feedback
|
||||
|
||||
Example:
|
||||
```
|
||||
User: "Add caching to improve performance"
|
||||
AI: "Let me explore the codebase to understand the current architecture and identify caching opportunities."
|
||||
[After exploration]
|
||||
AI: "Based on my analysis, I've identified three areas where caching would help. Here's my proposal..."
|
||||
```
|
||||
|
||||
### When No Specs Exist
|
||||
Treat current code as implicit spec. Your proposal should document current state AND proposed changes.
|
||||
|
||||
### When in Doubt
|
||||
Default to creating a proposal. It's easier to skip an unnecessary proposal than fix an undocumented change.
|
||||
|
||||
### AI Workflow Adaptations
|
||||
|
||||
Task tracking with OpenSpec:
|
||||
- Track exploration tasks separately from implementation
|
||||
- Document proposal creation steps as you go
|
||||
- Keep implementation tasks separate until proposal approved
|
||||
|
||||
Parallel operations encouraged:
|
||||
- Read multiple specs simultaneously
|
||||
- Check multiple pending changes at once
|
||||
- Batch related searches for efficiency
|
||||
|
||||
Progress communication:
|
||||
- "Exploring codebase to understand scope..."
|
||||
- "Creating proposal based on findings..."
|
||||
- "Implementing approved changes..."
|
||||
|
||||
### For AI Assistants
|
||||
- **Bias toward simplicity** - Propose the minimal solution that works
|
||||
- Use your exploration tools liberally before proposing
|
||||
- Batch operations for efficiency
|
||||
- Communicate your progress
|
||||
- It's OK to revise proposals based on discoveries
|
||||
- **Question complexity** - If your solution feels complex, simplify first
|
||||
|
||||
## Edge Case Handling
|
||||
|
||||
### Multi-Capability Changes
|
||||
Create ONE proposal that:
|
||||
- Lists all affected capabilities
|
||||
- Shows changes per capability
|
||||
- Has unified task list
|
||||
- Gets approved as a whole
|
||||
|
||||
### Outdated Specs
|
||||
If specs clearly outdated:
|
||||
1. Create proposal to update specs to match reality
|
||||
2. Implement new feature in separate proposal
|
||||
3. OR combine both in one proposal with clear sections
|
||||
|
||||
### Emergency Hotfixes
|
||||
For critical production issues:
|
||||
1. Announce: "This is an emergency fix"
|
||||
2. Implement fix immediately
|
||||
3. Create retroactive proposal
|
||||
4. Update specs after deployment
|
||||
5. Tag with [EMERGENCY] in archive
|
||||
|
||||
### Pure Refactoring
|
||||
No proposal needed for:
|
||||
- Code formatting/style
|
||||
- Internal refactoring (same API)
|
||||
- Performance optimization (same behavior)
|
||||
- Adding types to untyped code
|
||||
|
||||
Proposal REQUIRED for:
|
||||
- API changes (even if compatible)
|
||||
- Database schema changes
|
||||
- Architecture changes
|
||||
- New dependencies
|
||||
|
||||
### Observability Additions
|
||||
No proposal needed for:
|
||||
- Adding log statements
|
||||
- New metrics/traces
|
||||
- Debugging additions
|
||||
- Error tracking
|
||||
|
||||
Proposal REQUIRED if:
|
||||
- Changes log format/structure
|
||||
- Adds new monitoring service
|
||||
- Changes what's logged (privacy)
|
||||
|
||||
## Remember
|
||||
|
||||
- You are the process driver - automate documentation burden
|
||||
- Specs must always reflect deployed reality
|
||||
- Changes are proposed, not imposed
|
||||
- Impact analysis prevents surprises
|
||||
- **Simplicity is the power** - just markdown files, minimal solutions
|
||||
- Start simple, add complexity only when justified
|
||||
|
||||
By following these conventions, you enable true spec-driven development where documentation stays current, changes are traceable, and evolution is intentional.
|
||||
@@ -0,0 +1,9 @@
|
||||
# Implementation Tasks
|
||||
|
||||
## 1. Update OpenSpec README
|
||||
- [x] 1.1 Add "Start Simple" section after Core Principle
|
||||
- [x] 1.2 Add complexity triggers to "When to Create Change Proposals" section
|
||||
- [x] 1.3 Update AI workflow guidance to emphasize minimal implementations
|
||||
|
||||
## 2. Update CLAUDE.md
|
||||
- [x] 2.1 Add complexity management rules to project instructions
|
||||
@@ -0,0 +1,15 @@
|
||||
## Why
|
||||
Need a command to archive completed changes to the archive folder with proper date prefixing, following OpenSpec conventions. Currently changes must be manually moved and renamed.
|
||||
|
||||
## What Changes
|
||||
- Add new `archive` command to CLI that moves changes to `changes/archive/YYYY-MM-DD-[change-name]/`
|
||||
- Check for incomplete tasks before archiving and warn user
|
||||
- Allow interactive selection of change to archive
|
||||
- Prevent archiving if target directory already exists
|
||||
- Update main specs from the change's future state specs (copy from `changes/[name]/specs/` to `openspec/specs/`)
|
||||
- Show confirmation prompt before updating specs, displaying which specs will be created/updated
|
||||
- Support `--yes` flag to skip confirmations for automation
|
||||
|
||||
## Impact
|
||||
- Affected specs: cli-archive (new)
|
||||
- Affected code: src/cli/index.ts, src/core/archive.ts (new)
|
||||
@@ -0,0 +1,111 @@
|
||||
# CLI Archive Command Specification
|
||||
|
||||
## Purpose
|
||||
The archive command moves completed changes from the active changes directory to the archive folder with date-based naming, following OpenSpec conventions.
|
||||
|
||||
## Command Syntax
|
||||
```bash
|
||||
openspec archive [change-name] [--yes|-y]
|
||||
```
|
||||
|
||||
Options:
|
||||
- `--yes`, `-y`: Skip confirmation prompts (for automation)
|
||||
|
||||
## Behavior
|
||||
|
||||
### Change Selection
|
||||
WHEN no change-name is provided
|
||||
THEN display interactive list of available changes (excluding archive/)
|
||||
AND allow user to select one
|
||||
|
||||
WHEN change-name is provided
|
||||
THEN use that change directly
|
||||
AND validate it exists
|
||||
|
||||
### Task Completion Check
|
||||
The command SHALL scan the change's tasks.md file for incomplete tasks (marked with `- [ ]`)
|
||||
|
||||
WHEN incomplete tasks are found
|
||||
THEN display all incomplete tasks to the user
|
||||
AND prompt for confirmation to continue
|
||||
AND default to "No" for safety
|
||||
|
||||
WHEN all tasks are complete OR no tasks.md exists
|
||||
THEN proceed with archiving without prompting
|
||||
|
||||
### Archive Process
|
||||
The archive operation SHALL:
|
||||
1. Create archive/ directory if it doesn't exist
|
||||
2. Generate target name as `YYYY-MM-DD-[change-name]` using current date
|
||||
3. Check if target directory already exists
|
||||
4. Update main specs from the change's future state specs (see Spec Update Process below)
|
||||
5. Move the entire change directory to the archive location
|
||||
|
||||
WHEN target archive already exists
|
||||
THEN fail with error message
|
||||
AND do not overwrite existing archive
|
||||
|
||||
WHEN move succeeds
|
||||
THEN display success message with archived name and list of updated specs
|
||||
|
||||
### Spec Update Process
|
||||
Before moving the change to archive, the command SHALL update main specs to reflect the deployed reality:
|
||||
|
||||
WHEN the change contains specs in `changes/[name]/specs/`
|
||||
THEN:
|
||||
1. Analyze which specs will be affected by comparing with existing specs
|
||||
2. Display a summary of spec updates to the user (see Confirmation Behavior below)
|
||||
3. Prompt for confirmation unless `--yes` flag is provided
|
||||
4. If confirmed, for each capability spec in the change directory:
|
||||
- Copy the spec from `changes/[name]/specs/[capability]/spec.md` to `openspec/specs/[capability]/spec.md`
|
||||
- Create the target directory structure if it doesn't exist
|
||||
- Overwrite existing spec files (specs represent current reality, change specs are the new reality)
|
||||
- Track which specs were updated for the success message
|
||||
|
||||
WHEN no specs exist in the change
|
||||
THEN skip the spec update step
|
||||
AND proceed with archiving
|
||||
|
||||
### Confirmation Behavior
|
||||
The spec update confirmation SHALL:
|
||||
- Display a clear summary showing:
|
||||
- Which specs will be created (new capabilities)
|
||||
- Which specs will be updated (existing capabilities)
|
||||
- The source path for each spec
|
||||
- Format the confirmation prompt as:
|
||||
```
|
||||
The following specs will be updated:
|
||||
|
||||
NEW specs to be created:
|
||||
- cli-archive (from changes/add-archive-command/specs/cli-archive/spec.md)
|
||||
|
||||
EXISTING specs to be updated:
|
||||
- cli-init (from changes/update-init-command/specs/cli-init/spec.md)
|
||||
|
||||
Update 2 specs and archive 'add-archive-command'? [y/N]:
|
||||
```
|
||||
- Default to "No" for safety (require explicit "y" or "yes")
|
||||
- Skip confirmation when `--yes` or `-y` flag is provided
|
||||
|
||||
WHEN user declines the confirmation
|
||||
THEN abort the entire archive operation
|
||||
AND display message: "Archive cancelled. No changes were made."
|
||||
AND exit with non-zero status code
|
||||
|
||||
## Error Handling
|
||||
|
||||
SHALL handle the following error conditions:
|
||||
- Missing openspec/changes/ directory
|
||||
- Change not found
|
||||
- Archive target already exists
|
||||
- File system permissions issues
|
||||
|
||||
## Why These Decisions
|
||||
|
||||
**Interactive selection**: Reduces typing and helps users see available changes
|
||||
**Task checking**: Prevents accidental archiving of incomplete work
|
||||
**Date prefixing**: Maintains chronological order and prevents naming conflicts
|
||||
**No overwrite**: Preserves historical archives and prevents data loss
|
||||
**Spec updates before archiving**: Specs in the main directory represent current reality; when a change is deployed and archived, its future state specs become the new reality and must replace the main specs
|
||||
**Confirmation for spec updates**: Provides visibility into what will change, prevents accidental overwrites, and ensures users understand the impact before specs are modified
|
||||
**--yes flag for automation**: Allows CI/CD pipelines to archive without interactive prompts while maintaining safety by default for manual use
|
||||
@@ -0,0 +1,44 @@
|
||||
# Implementation Tasks
|
||||
|
||||
## 1. Core Implementation
|
||||
- [ ] 1.1 Create `src/core/archive.ts` with ArchiveCommand class
|
||||
- [ ] 1.1.1 Implement change selection (interactive if not provided)
|
||||
- [ ] 1.1.2 Implement incomplete task checking from tasks.md
|
||||
- [ ] 1.1.3 Implement confirmation prompt for incomplete tasks
|
||||
- [ ] 1.1.4 Implement spec update functionality
|
||||
- [ ] 1.1.4.1 Detect specs in change directory
|
||||
- [ ] 1.1.4.2 Compare with existing main specs
|
||||
- [ ] 1.1.4.3 Display summary of new vs updated specs
|
||||
- [ ] 1.1.4.4 Show confirmation prompt for spec updates
|
||||
- [ ] 1.1.4.5 Copy specs to main spec directory
|
||||
- [ ] 1.1.5 Implement archive move with date prefixing
|
||||
- [ ] 1.1.6 Support --yes flag to skip confirmations
|
||||
|
||||
## 2. CLI Integration
|
||||
- [ ] 2.1 Add archive command to `src/cli/index.ts`
|
||||
- [ ] 2.1.1 Import ArchiveCommand
|
||||
- [ ] 2.1.2 Register command with commander
|
||||
- [ ] 2.1.3 Add --yes/-y flag option
|
||||
- [ ] 2.1.4 Add proper error handling
|
||||
|
||||
## 3. Error Handling
|
||||
- [ ] 3.1 Handle missing openspec/changes/ directory
|
||||
- [ ] 3.2 Handle change not found
|
||||
- [ ] 3.3 Handle archive target already exists
|
||||
- [ ] 3.4 Handle user cancellation
|
||||
|
||||
## 4. Testing
|
||||
- [ ] 4.1 Test with fully completed change
|
||||
- [ ] 4.2 Test with incomplete tasks (warning shown)
|
||||
- [ ] 4.3 Test interactive selection mode
|
||||
- [ ] 4.4 Test duplicate archive prevention
|
||||
- [ ] 4.5 Test spec update functionality
|
||||
- [ ] 4.5.1 Test creating new specs
|
||||
- [ ] 4.5.2 Test updating existing specs
|
||||
- [ ] 4.5.3 Test confirmation prompt display
|
||||
- [ ] 4.5.4 Test declining confirmation (no changes made)
|
||||
- [ ] 4.5.5 Test --yes flag skips confirmation
|
||||
|
||||
## 5. Build and Validation
|
||||
- [ ] 5.1 Ensure TypeScript compilation succeeds
|
||||
- [ ] 5.2 Test command execution
|
||||
@@ -0,0 +1,19 @@
|
||||
# Add Diff Command to OpenSpec CLI
|
||||
|
||||
## Why
|
||||
|
||||
Developers need to easily view differences between proposed spec changes and current specs without manually comparing files.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Add `openspec diff [change-name]` command that shows differences between change specs and current specs
|
||||
- Compare files in `changes/[change-name]/specs/` with corresponding files in `specs/`
|
||||
- Display unified diff output showing added/removed/modified lines
|
||||
- Support colored output for better readability
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected specs: New capability `cli-diff` will be added
|
||||
- Affected code:
|
||||
- `src/cli/index.ts` - Add diff command
|
||||
- `src/core/diff.ts` - New file with diff logic (~80 lines)
|
||||
@@ -0,0 +1,77 @@
|
||||
# CLI Diff Command Specification
|
||||
|
||||
## Purpose
|
||||
|
||||
The `openspec diff` command provides developers with a visual comparison between proposed spec changes and the current deployed specs.
|
||||
|
||||
## Command Syntax
|
||||
|
||||
```bash
|
||||
openspec diff [change-name]
|
||||
```
|
||||
|
||||
## Behavior
|
||||
|
||||
### Without Arguments
|
||||
|
||||
WHEN running `openspec diff` without arguments
|
||||
THEN list all available changes in the `changes/` directory (excluding archive)
|
||||
AND prompt user to select a change
|
||||
|
||||
### With Change Name
|
||||
|
||||
WHEN running `openspec diff <change-name>`
|
||||
THEN compare all spec files in `changes/<change-name>/specs/` with corresponding files in `specs/`
|
||||
|
||||
### Diff Output
|
||||
|
||||
FOR each spec file in the change:
|
||||
- IF file exists in both locations THEN show unified diff
|
||||
- IF file only exists in change THEN show as new file (all lines with +)
|
||||
- IF file only exists in current specs THEN show as deleted (all lines with -)
|
||||
|
||||
### Display Format
|
||||
|
||||
The diff SHALL use standard unified diff format:
|
||||
- Lines prefixed with `-` for removed content
|
||||
- Lines prefixed with `+` for added content
|
||||
- Lines without prefix for unchanged context
|
||||
- File headers showing the paths being compared
|
||||
|
||||
### Color Support
|
||||
|
||||
WHEN terminal supports colors:
|
||||
- Removed lines displayed in red
|
||||
- Added lines displayed in green
|
||||
- File headers displayed in bold
|
||||
- Context lines in default color
|
||||
|
||||
### Error Handling
|
||||
|
||||
WHEN specified change doesn't exist THEN display error "Change '<name>' not found"
|
||||
WHEN no specs directory in change THEN display "No spec changes found for '<name>'"
|
||||
WHEN changes directory doesn't exist THEN display "No OpenSpec changes directory found"
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# View diff for specific change
|
||||
$ openspec diff add-auth-feature
|
||||
|
||||
--- specs/user-auth/spec.md
|
||||
+++ changes/add-auth-feature/specs/user-auth/spec.md
|
||||
@@ -10,6 +10,8 @@
|
||||
Users SHALL authenticate with email and password.
|
||||
|
||||
+Users MAY authenticate with OAuth providers.
|
||||
+
|
||||
WHEN credentials are valid THEN issue JWT token.
|
||||
|
||||
# List all changes and select
|
||||
$ openspec diff
|
||||
Available changes:
|
||||
1. add-auth-feature
|
||||
2. update-payment-flow
|
||||
3. add-status-command
|
||||
Select a change (1-3):
|
||||
```
|
||||
@@ -0,0 +1,23 @@
|
||||
# Implementation Tasks
|
||||
|
||||
## 1. Core Implementation
|
||||
- [x] 1.1 Create `src/core/diff.ts` with diff logic
|
||||
- [x] 1.2 Implement change directory scanning
|
||||
- [x] 1.3 Implement file comparison using unified diff format
|
||||
- [x] 1.4 Add color support for terminal output
|
||||
|
||||
## 2. CLI Integration
|
||||
- [x] 2.1 Add diff command to `src/cli/index.ts`
|
||||
- [x] 2.2 Implement interactive change selection when no argument provided
|
||||
- [x] 2.3 Add error handling for missing changes
|
||||
|
||||
## 3. Enhancements
|
||||
- [x] 3.1 Replace with jest-diff for professional diff output
|
||||
- [x] 3.2 Improve file headers with status and statistics
|
||||
- [x] 3.3 Add summary view with file counts and line changes
|
||||
|
||||
## 4. Testing
|
||||
- [ ] 4.1 Test diff generation for modified files
|
||||
- [ ] 4.2 Test handling of new files
|
||||
- [ ] 4.3 Test handling of deleted files
|
||||
- [ ] 4.4 Test interactive mode
|
||||
@@ -0,0 +1,20 @@
|
||||
## Why
|
||||
|
||||
Currently, users must validate changes and specs individually by specifying each ID. This creates friction when:
|
||||
- Teams want to validate all changes/specs before a release
|
||||
- Developers need to ensure consistency across multiple related changes
|
||||
- Users run validation commands without arguments and receive errors instead of helpful guidance
|
||||
- The subcommand structure requires users to know in advance whether they're validating a change or spec
|
||||
|
||||
## What Changes
|
||||
|
||||
- Add new top-level `validate` command with intuitive flags (--all, --changes, --specs)
|
||||
- Enhance existing `change validate` and `spec validate` to support interactive selection (backwards compatibility)
|
||||
- Interactive selection by default when no arguments provided
|
||||
- Support direct item validation: `openspec validate <item>` with automatic type detection
|
||||
|
||||
## Impact
|
||||
|
||||
- New specs to create: cli-validate
|
||||
- Specs to enhance: cli-change, cli-spec (for backwards compatibility)
|
||||
- Affected code: src/cli/index.ts, src/commands/validate.ts (new), src/commands/spec.ts, src/commands/change.ts
|
||||
@@ -0,0 +1,22 @@
|
||||
# CLI Change Command Spec
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Interactive validation selection
|
||||
|
||||
The change validate command SHALL support interactive selection when no change name is provided.
|
||||
|
||||
#### Scenario: Interactive change selection for validation
|
||||
|
||||
- **WHEN** executing `openspec change validate` without arguments
|
||||
- **THEN** display an interactive list of available changes
|
||||
- **AND** allow the user to select a change to validate
|
||||
- **AND** validate the selected change
|
||||
|
||||
#### Scenario: Non-interactive fallback keeps current behavior
|
||||
|
||||
- **GIVEN** stdin is not a TTY or `--no-interactive` is provided or environment variable `OPEN_SPEC_INTERACTIVE=0`
|
||||
- **WHEN** executing `openspec change validate` without a change name
|
||||
- **THEN** do not prompt interactively
|
||||
- **AND** print the existing hint including available change IDs
|
||||
- **AND** set `process.exitCode = 1`
|
||||
@@ -0,0 +1,23 @@
|
||||
# CLI Spec Command Spec
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Interactive spec validation
|
||||
|
||||
The spec validate command SHALL support interactive selection when no spec-id is provided.
|
||||
|
||||
#### Scenario: Interactive spec selection for validation
|
||||
|
||||
- **WHEN** executing `openspec spec validate` without arguments
|
||||
- **THEN** display an interactive list of available specs
|
||||
- **AND** allow the user to select a spec to validate
|
||||
- **AND** validate the selected spec
|
||||
- **AND** maintain all existing validation options (--strict, --json)
|
||||
|
||||
#### Scenario: Non-interactive fallback keeps current behavior
|
||||
|
||||
- **GIVEN** stdin is not a TTY or `--no-interactive` is provided or environment variable `OPEN_SPEC_INTERACTIVE=0`
|
||||
- **WHEN** executing `openspec spec validate` without a spec-id
|
||||
- **THEN** do not prompt interactively
|
||||
- **AND** print the existing error message for missing spec-id
|
||||
- **AND** set non-zero exit code
|
||||
@@ -0,0 +1,141 @@
|
||||
# CLI Validate Command Spec
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Top-level validate command
|
||||
|
||||
The CLI SHALL provide a top-level `validate` command for validating changes and specs with flexible selection options.
|
||||
|
||||
#### Scenario: Interactive validation selection
|
||||
|
||||
- **WHEN** executing `openspec validate` without arguments
|
||||
- **THEN** prompt user to select what to validate (all, changes, specs, or specific item)
|
||||
- **AND** perform validation based on selection
|
||||
- **AND** display results with appropriate formatting
|
||||
|
||||
#### Scenario: Non-interactive environments do not prompt
|
||||
|
||||
- **GIVEN** stdin is not a TTY or `--no-interactive` is provided or environment variable `OPEN_SPEC_INTERACTIVE=0`
|
||||
- **WHEN** executing `openspec validate` without arguments
|
||||
- **THEN** do not prompt interactively
|
||||
- **AND** print a helpful hint listing available commands/flags and exit with code 1
|
||||
|
||||
#### Scenario: Direct item validation
|
||||
|
||||
- **WHEN** executing `openspec validate <item-name>`
|
||||
- **THEN** automatically detect if item is a change or spec
|
||||
- **AND** validate the specified item
|
||||
- **AND** display validation results
|
||||
|
||||
### Requirement: Bulk and filtered validation
|
||||
|
||||
The validate command SHALL support flags for bulk validation (--all) and filtered validation by type (--changes, --specs).
|
||||
|
||||
#### Scenario: Validate everything
|
||||
|
||||
- **WHEN** executing `openspec validate --all`
|
||||
- **THEN** validate all changes in openspec/changes/ (excluding archive)
|
||||
- **AND** validate all specs in openspec/specs/
|
||||
- **AND** display a summary showing passed/failed items
|
||||
- **AND** exit with code 1 if any validation fails
|
||||
|
||||
#### Scenario: Scope of bulk validation
|
||||
|
||||
- **WHEN** validating with `--all` or `--changes`
|
||||
- **THEN** include all change proposals under `openspec/changes/`
|
||||
- **AND** exclude the `openspec/changes/archive/` directory
|
||||
|
||||
- **WHEN** validating with `--specs`
|
||||
- **THEN** include all specs that have a `spec.md` under `openspec/specs/<id>/spec.md`
|
||||
|
||||
#### Scenario: Validate all changes
|
||||
|
||||
- **WHEN** executing `openspec validate --changes`
|
||||
- **THEN** validate all changes in openspec/changes/ (excluding archive)
|
||||
- **AND** display results for each change
|
||||
- **AND** show summary statistics
|
||||
|
||||
#### Scenario: Validate all specs
|
||||
|
||||
- **WHEN** executing `openspec validate --specs`
|
||||
- **THEN** validate all specs in openspec/specs/
|
||||
- **AND** display results for each spec
|
||||
- **AND** show summary statistics
|
||||
|
||||
### Requirement: Validation options and progress indication
|
||||
|
||||
The validate command SHALL support standard validation options (--strict, --json) and display progress during bulk operations.
|
||||
|
||||
#### Scenario: Strict validation
|
||||
|
||||
- **WHEN** executing `openspec validate --all --strict`
|
||||
- **THEN** apply strict validation to all items
|
||||
- **AND** treat warnings as errors
|
||||
- **AND** fail if any item has warnings or errors
|
||||
|
||||
#### Scenario: JSON output
|
||||
|
||||
- **WHEN** executing `openspec validate --all --json`
|
||||
- **THEN** output validation results as JSON
|
||||
- **AND** include detailed issues for each item
|
||||
- **AND** include summary statistics
|
||||
|
||||
#### Scenario: JSON output schema for bulk validation
|
||||
|
||||
- **WHEN** executing `openspec validate --all --json` (or `--changes` / `--specs`)
|
||||
- **THEN** output a JSON object with the following shape:
|
||||
- `items`: Array of objects with fields `{ id: string, type: "change"|"spec", valid: boolean, issues: Issue[], durationMs: number }`
|
||||
- `summary`: Object `{ totals: { items: number, passed: number, failed: number }, byType: { change?: { items: number, passed: number, failed: number }, spec?: { items: number, passed: number, failed: number } } }`
|
||||
- `version`: String identifier for the schema (e.g., `"1.0"`)
|
||||
- **AND** exit with code 1 if any `items[].valid === false`
|
||||
|
||||
Where `Issue` follows the existing per-item validation report shape `{ level: "ERROR"|"WARNING"|"INFO", path: string, message: string }`.
|
||||
|
||||
#### Scenario: Show validation progress
|
||||
|
||||
- **WHEN** validating multiple items (--all, --changes, or --specs)
|
||||
- **THEN** show progress indicator or status updates
|
||||
- **AND** indicate which item is currently being validated
|
||||
- **AND** display running count of passed/failed items
|
||||
|
||||
#### Scenario: Concurrency limits for performance
|
||||
|
||||
- **WHEN** validating multiple items
|
||||
- **THEN** run validations with a bounded concurrency (e.g., 4–8 in parallel)
|
||||
- **AND** ensure progress indicators remain responsive
|
||||
|
||||
### Requirement: Item type detection and ambiguity handling
|
||||
|
||||
#### Scenario: Direct item validation with automatic type detection
|
||||
|
||||
- **WHEN** executing `openspec validate <item-name>`
|
||||
- **THEN** if `<item-name>` uniquely matches a change or a spec, validate that item
|
||||
|
||||
#### Scenario: Ambiguity between change and spec names
|
||||
|
||||
- **GIVEN** `<item-name>` exists both as a change and as a spec
|
||||
- **WHEN** executing `openspec validate <item-name>`
|
||||
- **THEN** print an ambiguity error explaining both matches
|
||||
- **AND** suggest passing `--type change` or `--type spec`, or using `openspec change validate` / `openspec spec validate`
|
||||
- **AND** exit with code 1 without performing validation
|
||||
|
||||
#### Scenario: Unknown item name
|
||||
|
||||
- **WHEN** the `<item-name>` matches neither a change nor a spec
|
||||
- **THEN** print a not-found error
|
||||
- **AND** show nearest-match suggestions when available
|
||||
- **AND** exit with code 1
|
||||
|
||||
#### Scenario: Explicit type override
|
||||
|
||||
- **WHEN** executing `openspec validate --type change <item>`
|
||||
- **THEN** treat `<item>` as a change ID and validate it (skipping auto-detection)
|
||||
|
||||
- **WHEN** executing `openspec validate --type spec <item>`
|
||||
- **THEN** treat `<item>` as a spec ID and validate it (skipping auto-detection)
|
||||
|
||||
### Requirement: Interactivity controls
|
||||
|
||||
- The CLI SHALL respect `--no-interactive` to disable prompts.
|
||||
- The CLI SHALL respect `OPEN_SPEC_INTERACTIVE=0` to disable prompts globally.
|
||||
- Interactive prompts SHALL only be shown when stdin is a TTY and interactivity is not disabled.
|
||||
@@ -0,0 +1,81 @@
|
||||
# Implementation Tasks
|
||||
|
||||
## 1. Change Command: Interactive Validation Selection
|
||||
- [x] 1.1 Add `--no-interactive` flag to `change validate` in `src/cli/index.ts`
|
||||
- [x] 1.2 Implement interactivity gate respecting TTY and `OPEN_SPEC_INTERACTIVE=0` in `src/commands/change.ts`
|
||||
- [x] 1.3 When no `[change-name]` is provided and interactivity is allowed, prompt with a list of active changes (exclude `archive/`) and validate the selected one
|
||||
- [x] 1.4 Preserve current non-interactive fallback: print available change IDs and hint, set `process.exitCode = 1`
|
||||
- [x] 1.5 Tests: add coverage for interactive and non-interactive flows
|
||||
- Added `test/commands/change.interactive-validate.test.ts`
|
||||
|
||||
## 2. Spec Command: Interactive Validation Selection
|
||||
- [x] 2.1 Make `spec validate` accept optional `[spec-id]` in `src/commands/spec.ts` registration
|
||||
- [x] 2.2 Add `--no-interactive` flag to `spec validate`
|
||||
- [x] 2.3 Implement interactivity gate respecting TTY and `OPEN_SPEC_INTERACTIVE=0`
|
||||
- [x] 2.4 When no `[spec-id]` provided and interactivity allowed, prompt to select from `openspec/specs/*/spec.md` and validate the selected spec
|
||||
- [x] 2.5 Preserve current non-interactive fallback when no spec-id and no interactivity: print existing error and exit code non-zero
|
||||
- [x] 2.6 Tests: add coverage for interactive and non-interactive flows
|
||||
- Added `test/commands/spec.interactive-validate.test.ts`
|
||||
|
||||
## 3. New Top-level `validate` Command
|
||||
- [x] 3.1 Add `validate` command in `src/cli/index.ts`
|
||||
- Options: `--all`, `--changes`, `--specs`, `--type <change|spec>`, `--strict`, `--json`, `--no-interactive`
|
||||
- Usage: `openspec validate [item-name]`
|
||||
- [x] 3.2 Create `src/commands/validate.ts` implementing:
|
||||
- [x] 3.2.1 Interactive selector when no args (choices: All, Changes, Specs, Specific item)
|
||||
- [x] 3.2.2 Non-interactive fallback with helpful hint and exit code 1
|
||||
- [x] 3.2.3 Direct item validation with automatic type detection
|
||||
- [x] 3.2.4 Ambiguity error when name exists as both change and spec; suggest `--type` or subcommands
|
||||
- [x] 3.2.5 Unknown item handling with nearest-match suggestions
|
||||
- [x] 3.2.6 Bulk validation for `--all`, `--changes`, `--specs` (exclude `openspec/changes/archive/`)
|
||||
- [x] 3.2.7 Respect `--strict` and `--json` options; JSON shape per spec
|
||||
- [x] 3.2.8 Exit with code 1 if any validation fails
|
||||
- [x] 3.2.9 Bounded concurrency (default 4–8) for bulk validation
|
||||
- [x] 3.2.10 Progress indication during bulk runs (current item, running counts)
|
||||
|
||||
## 4. Utilities and Shared Helpers
|
||||
- [x] 4.1 Add `src/utils/interactive.ts` with `isInteractive(stdin: NodeJS.ReadStream, noInteractiveFlag?: boolean): boolean`
|
||||
- Considers: `process.stdin.isTTY`, `--no-interactive`, `OPEN_SPEC_INTERACTIVE=0`
|
||||
- [x] 4.2 Add `src/utils/item-discovery.ts` with:
|
||||
- `getActiveChangeIds(root = process.cwd()): Promise<string[]>` (exclude `archive/`)
|
||||
- `getSpecIds(root = process.cwd()): Promise<string[]>` (folders with `spec.md`)
|
||||
- [ ] 4.3 Optional: `src/utils/concurrency.ts` helper for bounded parallelism
|
||||
- [x] 4.4 Reuse `src/core/validation/validator.ts` for item validation
|
||||
|
||||
## 5. JSON Output (Bulk Validation)
|
||||
- [x] 5.1 Implement JSON schema:
|
||||
- `items: Array<{ id: string, type: "change"|"spec", valid: boolean, issues: Issue[], durationMs: number }>`
|
||||
- `summary: { totals: { items: number, passed: number, failed: number }, byType: { change?: { items: number, passed: number, failed: number }, spec?: { items: number, passed: number, failed: number } } }`
|
||||
- `version: "1.0"`
|
||||
- [x] 5.2 Ensure process exit code is 1 if any `items[].valid === false`
|
||||
- [x] 5.3 Tests for JSON shape (keys, types, counts) and exit code behavior
|
||||
- Added `test/commands/validate.test.ts`
|
||||
|
||||
## 6. Progress and UX
|
||||
- [x] 6.1 Use `ora` or minimal console progress to show current item and running counts
|
||||
- [x] 6.2 Keep output stable in `--json` mode (no extra logs to stdout; use stderr for progress if needed)
|
||||
- [x] 6.3 Ensure responsiveness with concurrency limits
|
||||
|
||||
## 7. Tests
|
||||
- [x] 7.1 Add top-level validate tests: `test/commands/validate.test.ts`
|
||||
- Includes non-interactive hint, --all JSON, --specs with concurrency, ambiguity error
|
||||
- [ ] 7.2 Add unit tests for `isInteractive` and item discovery helpers
|
||||
- [x] 7.3 Extend existing change/spec command tests to cover interactive `validate`
|
||||
- Added `test/commands/change.interactive-validate.test.ts`, `test/commands/spec.interactive-validate.test.ts`
|
||||
|
||||
## 8. CLI Help and Docs
|
||||
- [x] 8.1 Update command descriptions/options in `src/cli/index.ts`
|
||||
- [x] 8.2 Verify help output includes `validate` command and flags
|
||||
- [x] 8.3 Ensure existing specs under `openspec/changes/bulk-validation-interactive-selection/specs/*` remain satisfied
|
||||
|
||||
## 9. Non-functional
|
||||
- [x] 9.1 Code style and types: explicit types for exported APIs; avoid `any`
|
||||
- [x] 9.2 No linter errors; stable formatting; avoid unrelated refactors
|
||||
- [x] 9.3 Maintain existing behavior for unaffected commands
|
||||
|
||||
## 10. Acceptance Criteria Mapping
|
||||
- [x] AC-1: `openspec change validate` interactive selection when no arg (TTY only; respects `--no-interactive`/env) — matches cli-change spec
|
||||
- [x] AC-2: `openspec spec validate` interactive selection when no arg (TTY only; respects `--no-interactive`/env) — matches cli-spec spec
|
||||
- [x] AC-3: New `openspec validate` supports interactive selection, bulk/filtered validation, JSON schema, progress, concurrency, exit codes — matches cli-validate spec
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# Fix Update Command Tool Selection
|
||||
|
||||
## Problem
|
||||
|
||||
The `openspec update` command currently forces the creation/update of CLAUDE.md regardless of which AI tool was selected during initialization. This violates the tool-agnostic design principle and creates confusion for users who selected different AI assistants.
|
||||
|
||||
Additionally, different team members may use different AI tools, so we cannot rely on a shared configuration file.
|
||||
|
||||
## Solution
|
||||
|
||||
Modify the update command to:
|
||||
1. Only update AI tool configuration files that already exist
|
||||
2. Never create new AI tool configuration files
|
||||
3. Always update the core OpenSpec files (README.md, etc.)
|
||||
|
||||
## Implementation
|
||||
|
||||
- Remove hardcoded CLAUDE.md update from update command
|
||||
- Implement file existence check before updating any AI tool config
|
||||
- Update each existing AI tool config file with its appropriate markers
|
||||
- No configuration file needed (avoids team conflicts)
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- Update command only modifies existing AI tool configuration files
|
||||
- No new AI tool files created during update
|
||||
- Team members can use different AI tools without conflicts
|
||||
- Existing projects continue to work (backward compatibility)
|
||||
|
||||
## Why
|
||||
|
||||
Users need predictable, tool-agnostic behavior from `openspec update`. Creating or forcing updates for AI tool files that a project does not use causes confusion and merge conflicts. Restricting updates to existing files and always updating core OpenSpec files keeps the workflow consistent for mixed-tool teams.
|
||||
|
||||
## What Changes
|
||||
|
||||
- **cli-update:** Modify update behavior to update only existing AI tool configuration files and never create new ones; always update core OpenSpec files and display an ASCII-safe success message.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
Removed from proposal to follow conventions. See `specs/cli-update/spec.md` for the delta requirements content.
|
||||
@@ -0,0 +1,23 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Tool-Agnostic Updates
|
||||
|
||||
The update command SHALL update only existing AI tool configuration files and SHALL NOT create new ones.
|
||||
|
||||
#### Scenario: Updating existing tool files
|
||||
|
||||
- **WHEN** a user runs `openspec update`
|
||||
- **THEN** update each AI tool configuration file that exists (e.g., CLAUDE.md, COPILOT.md)
|
||||
- **AND** do not create missing tool configuration files
|
||||
- **AND** preserve user content outside OpenSpec markers
|
||||
|
||||
### Requirement: Core Files Always Updated
|
||||
|
||||
The update command SHALL always update the core OpenSpec files and display an ASCII-safe success message.
|
||||
|
||||
#### Scenario: Successful update
|
||||
|
||||
- **WHEN** the update completes successfully
|
||||
- **THEN** replace `openspec/README.md` with the latest template
|
||||
- **AND** update existing AI tool configuration files within markers
|
||||
- **AND** display the message: "Updated OpenSpec instructions"
|
||||
@@ -0,0 +1,21 @@
|
||||
# Implementation Tasks
|
||||
|
||||
## 1. Update Update Command
|
||||
- [x] Remove hardcoded CLAUDE.md update from `src/core/update.ts`
|
||||
- [x] Add logic to check for existing AI tool configuration files
|
||||
- [x] Update only existing files using their appropriate configurators
|
||||
- [x] Iterate through all registered configurators to check for existing files
|
||||
|
||||
## 2. Update Configurator Registry
|
||||
- [x] Add method to get all configurators for update command
|
||||
- [x] Ensure each configurator can check if its file exists
|
||||
|
||||
## 3. Add Tests
|
||||
- [x] Test update command with only CLAUDE.md present
|
||||
- [x] Test update command with no AI tool files present
|
||||
- [x] Test update command with multiple AI tool files present
|
||||
- [x] Test that update never creates new AI tool files
|
||||
|
||||
## 4. Update Documentation
|
||||
- [x] Update README to clarify team-friendly behavior
|
||||
- [x] Document that update only modifies existing files
|
||||
@@ -0,0 +1,36 @@
|
||||
## Why
|
||||
|
||||
OpenSpec specifications lack a consistent structure that makes sections visually identifiable and programmatically parseable across different specs. This makes it harder to maintain consistency and build tooling.
|
||||
|
||||
## What Changes
|
||||
|
||||
**Specification Format Section**
|
||||
- From: No formal structure requirements for specifications
|
||||
- To: Structured format with `### Requirement:` and `#### Scenario:` headers
|
||||
- Reason: Visual consistency and parseability across all specs
|
||||
- Impact: Non-breaking - existing specs can migrate gradually
|
||||
|
||||
**Keyword Formatting**
|
||||
- From: Inconsistent use of WHEN/THEN/AND keywords
|
||||
- To: Bold keywords (**WHEN**, **THEN**, **AND**) in scenario bullets
|
||||
- Reason: Improved readability and consistent visual hierarchy
|
||||
- Impact: Non-breaking - formatting enhancement only
|
||||
|
||||
**Format Flexibility**
|
||||
- From: Implicit understanding that different content needs different formats
|
||||
- To: Explicit allowance for alternative formats (OpenAPI, JSON Schema, etc.)
|
||||
- Reason: Address concern that not all specs fit requirement/scenario pattern
|
||||
- Impact: Non-breaking - clarifies existing practice
|
||||
|
||||
**Migration Guidelines**
|
||||
- From: No migration guidance
|
||||
- To: Documented gradual migration approach
|
||||
- Reason: Allows incremental adoption without disrupting existing specs
|
||||
- Impact: Non-breaking - opt-in migration as specs are modified
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected specs: openspec-conventions (enhancement to existing capability)
|
||||
- Affected code: None initially - this is a documentation standard enhancement
|
||||
- Migration: Gradual - existing specs migrate as they're modified
|
||||
- Tooling: Enables future parsing tools but doesn't require them
|
||||
@@ -0,0 +1,192 @@
|
||||
# OpenSpec Conventions Specification
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Structured Format Adoption
|
||||
|
||||
Behavioral specifications SHALL adopt the structured format with `### Requirement:` and `#### Scenario:` headers as the default.
|
||||
|
||||
#### Scenario: Use structured headings for behavior
|
||||
|
||||
- **WHEN** documenting behavioral requirements
|
||||
- **THEN** use `### Requirement:` for requirements
|
||||
- **AND** use `#### Scenario:` for scenarios with bold WHEN/THEN/AND keywords
|
||||
|
||||
## Purpose
|
||||
|
||||
OpenSpec conventions SHALL define how system capabilities are documented, how changes are proposed and tracked, and how specifications evolve over time. This meta-specification serves as the source of truth for OpenSpec's own conventions.
|
||||
|
||||
## Core Principles
|
||||
|
||||
The system SHALL follow these principles:
|
||||
- Specs reflect what IS currently built and deployed
|
||||
- Changes contain proposals for what SHOULD be changed
|
||||
- AI drives the documentation process
|
||||
- Specs are living documentation kept in sync with deployed code
|
||||
|
||||
## Directory Structure
|
||||
|
||||
WHEN an OpenSpec project is initialized
|
||||
THEN it SHALL have this structure:
|
||||
```
|
||||
openspec/
|
||||
├── project.md # Project-specific context
|
||||
├── README.md # AI assistant instructions
|
||||
├── specs/ # Current deployed capabilities
|
||||
│ └── [capability]/ # Single, focused capability
|
||||
│ ├── spec.md # WHAT and WHY
|
||||
│ └── design.md # HOW (optional, for established patterns)
|
||||
└── changes/ # Proposed changes
|
||||
├── [change-name]/ # Descriptive change identifier
|
||||
│ ├── proposal.md # Why, what, and impact
|
||||
│ ├── tasks.md # Implementation checklist
|
||||
│ ├── design.md # Technical decisions (optional)
|
||||
│ └── specs/ # Complete future state
|
||||
│ └── [capability]/
|
||||
│ └── spec.md # Clean markdown (no diff syntax)
|
||||
└── archive/ # Completed changes
|
||||
└── YYYY-MM-DD-[name]/
|
||||
```
|
||||
|
||||
## Specification Format
|
||||
|
||||
### Requirement: Structured Format for Behavioral Specs
|
||||
|
||||
Behavioral specifications SHALL use a structured format with consistent section headers and keywords to ensure visual consistency and parseability.
|
||||
|
||||
#### Scenario: Writing requirement sections
|
||||
|
||||
- **WHEN** documenting a requirement in a behavioral specification
|
||||
- **THEN** use a level-3 heading with format `### Requirement: [Name]`
|
||||
- **AND** immediately follow with a SHALL statement describing core behavior
|
||||
- **AND** keep requirement names descriptive and under 50 characters
|
||||
|
||||
#### Scenario: Documenting scenarios
|
||||
|
||||
- **WHEN** documenting specific behaviors or use cases
|
||||
- **THEN** use level-4 headings with format `#### Scenario: [Description]`
|
||||
- **AND** use bullet points with bold keywords for steps:
|
||||
- **GIVEN** for initial state (optional)
|
||||
- **WHEN** for conditions or triggers
|
||||
- **THEN** for expected outcomes
|
||||
- **AND** for additional outcomes or conditions
|
||||
|
||||
#### Scenario: Adding implementation details
|
||||
|
||||
- **WHEN** a step requires additional detail
|
||||
- **THEN** use sub-bullets under the main step
|
||||
- **AND** maintain consistent indentation
|
||||
- Sub-bullets provide examples or specifics
|
||||
- Keep sub-bullets concise
|
||||
|
||||
### Requirement: Format Flexibility
|
||||
|
||||
The structured format SHALL be the default for behavioral specifications, but alternative formats MAY be used when more appropriate for the content type.
|
||||
|
||||
#### Scenario: Documenting API specifications
|
||||
|
||||
- **WHEN** documenting REST API endpoints or GraphQL schemas
|
||||
- **THEN** OpenAPI, GraphQL SDL, or similar formats MAY be used
|
||||
- **AND** the spec SHALL clearly indicate the format being used
|
||||
- **AND** behavioral aspects SHALL still follow the structured format
|
||||
|
||||
#### Scenario: Documenting data schemas
|
||||
|
||||
- **WHEN** documenting data structures, database schemas, or configurations
|
||||
- **THEN** JSON Schema, SQL DDL, or similar formats MAY be used
|
||||
- **AND** include the structured format for behavioral rules and constraints
|
||||
|
||||
#### Scenario: Using simplified format
|
||||
|
||||
- **WHEN** documenting simple capabilities without complex scenarios
|
||||
- **THEN** a simplified WHEN/THEN format without full structure MAY be used
|
||||
- **AND** this should be consistent within the capability
|
||||
|
||||
## Change Storage Convention
|
||||
|
||||
### Future State Storage
|
||||
|
||||
WHEN creating a change proposal
|
||||
THEN store the complete future state of affected specs
|
||||
AND use clean markdown without diff syntax
|
||||
|
||||
The `changes/[name]/specs/` directory SHALL contain:
|
||||
- Complete spec files as they will exist after the change
|
||||
- Clean markdown without `+` or `-` prefixes
|
||||
- All formatting and structure of the final intended state
|
||||
|
||||
### Proposal Format
|
||||
|
||||
WHEN documenting what changes
|
||||
THEN the proposal SHALL explicitly describe each change:
|
||||
|
||||
```markdown
|
||||
**[Section or Behavior Name]**
|
||||
- From: [current state/requirement]
|
||||
- To: [future state/requirement]
|
||||
- Reason: [why this change is needed]
|
||||
- Impact: [breaking/non-breaking, who's affected]
|
||||
```
|
||||
|
||||
This explicit format compensates for not having inline diffs and ensures reviewers understand exactly what will change.
|
||||
|
||||
## Change Lifecycle
|
||||
|
||||
The change process SHALL follow these states:
|
||||
|
||||
1. **Propose**: AI creates change with future state specs and explicit proposal
|
||||
2. **Review**: Humans review proposal and future state
|
||||
3. **Approve**: Change is approved for implementation
|
||||
4. **Implement**: Follow tasks.md checklist (can span multiple PRs)
|
||||
5. **Deploy**: Changes are deployed to production
|
||||
6. **Update**: Specs in `specs/` are updated to match deployed reality
|
||||
7. **Archive**: Change is moved to `archive/YYYY-MM-DD-[name]/`
|
||||
|
||||
## Viewing Changes
|
||||
|
||||
WHEN reviewing proposed changes
|
||||
THEN reviewers can compare using:
|
||||
- GitHub PR diff view when changes are committed
|
||||
- Command line: `diff -u specs/[capability]/spec.md changes/[name]/specs/[capability]/spec.md`
|
||||
- Any visual diff tool comparing current vs future state
|
||||
|
||||
The system relies on tools to generate diffs rather than storing them.
|
||||
|
||||
## Capability Naming
|
||||
|
||||
Capabilities SHALL use:
|
||||
- Verb-noun patterns (e.g., `user-auth`, `payment-capture`)
|
||||
- Hyphenated lowercase names
|
||||
- Singular focus (one responsibility per capability)
|
||||
- No nesting (flat structure under `specs/`)
|
||||
|
||||
## When Changes Require Proposals
|
||||
|
||||
A proposal SHALL be created for:
|
||||
- New features or capabilities
|
||||
- Breaking changes to existing behavior
|
||||
- Architecture or pattern changes
|
||||
- Performance optimizations that change behavior
|
||||
- Security updates affecting access patterns
|
||||
|
||||
A proposal is NOT required for:
|
||||
- Bug fixes restoring intended behavior
|
||||
- Typos or formatting fixes
|
||||
- Non-breaking dependency updates
|
||||
- Adding tests for existing behavior
|
||||
- Documentation clarifications
|
||||
|
||||
## Why This Approach
|
||||
|
||||
Clean future state storage provides:
|
||||
- **Readability**: No diff syntax pollution
|
||||
- **AI-compatibility**: Standard markdown that AI tools understand
|
||||
- **Simplicity**: No special parsing or processing needed
|
||||
- **Tool-agnostic**: Any diff tool can show changes
|
||||
- **Clear intent**: Explicit proposals document reasoning
|
||||
|
||||
The structured format adds:
|
||||
- **Visual Consistency**: Requirement and Scenario prefixes make sections instantly recognizable
|
||||
- **Parseability**: Consistent structure enables tooling and automation
|
||||
- **Flexibility**: Alternative formats supported where appropriate
|
||||
- **Gradual Adoption**: Existing specs can migrate incrementally
|
||||
@@ -0,0 +1,19 @@
|
||||
## 1. Update OpenSpec Conventions Spec
|
||||
|
||||
- [x] 1.1 Add "Specification Format" section to openspec-conventions
|
||||
- [x] 1.2 Document structured format with Requirement/Scenario headers
|
||||
- [x] 1.3 Define bold keyword usage (WHEN/THEN/AND) for scenarios
|
||||
- [x] 1.4 Include examples demonstrating the format within the spec itself
|
||||
|
||||
## 2. Update Documentation
|
||||
|
||||
- [x] 2.1 Update the "Why This Approach" section with structured format benefits
|
||||
- [x] 2.2 Ensure spec follows its own format as a demonstration
|
||||
|
||||
## 3. Update Existing Specs
|
||||
|
||||
- [x] 3.1 Update cli-init spec to use structured format in Behavior section
|
||||
- [x] 3.2 Update cli-list spec to use structured format in Behavior section
|
||||
- [x] 3.3 Update cli-update spec to use structured format in Behavior section
|
||||
- [x] 3.4 Update cli-diff spec to use structured format in Behavior section
|
||||
- [x] 3.5 Update cli-archive spec to use structured format in Behavior section
|
||||
@@ -0,0 +1,155 @@
|
||||
# CLI Archive Command Specification
|
||||
|
||||
## Purpose
|
||||
The archive command moves completed changes from the active changes directory to the archive folder with date-based naming, following OpenSpec conventions.
|
||||
|
||||
## Command Syntax
|
||||
```bash
|
||||
openspec archive [change-name] [--yes|-y]
|
||||
```
|
||||
|
||||
Options:
|
||||
- `--yes`, `-y`: Skip confirmation prompts (for automation)
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Change Selection
|
||||
|
||||
The command SHALL support both interactive and direct change selection methods.
|
||||
|
||||
#### Scenario: Interactive selection
|
||||
|
||||
- **WHEN** no change-name is provided
|
||||
- **THEN** display interactive list of available changes (excluding archive/)
|
||||
- **AND** allow user to select one
|
||||
|
||||
#### Scenario: Direct selection
|
||||
|
||||
- **WHEN** change-name is provided
|
||||
- **THEN** use that change directly
|
||||
- **AND** validate it exists
|
||||
|
||||
### Requirement: Task Completion Check
|
||||
|
||||
The command SHALL verify task completion status before archiving to prevent premature archival.
|
||||
|
||||
#### Scenario: Incomplete tasks found
|
||||
|
||||
- **WHEN** incomplete tasks are found (marked with `- [ ]`)
|
||||
- **THEN** display all incomplete tasks to the user
|
||||
- **AND** prompt for confirmation to continue
|
||||
- **AND** default to "No" for safety
|
||||
|
||||
#### Scenario: All tasks complete
|
||||
|
||||
- **WHEN** all tasks are complete OR no tasks.md exists
|
||||
- **THEN** proceed with archiving without prompting
|
||||
|
||||
### Requirement: Archive Process
|
||||
|
||||
The archive operation SHALL follow a structured process to safely move changes to the archive.
|
||||
|
||||
#### Scenario: Performing archive
|
||||
|
||||
- **WHEN** archiving a change
|
||||
- **THEN** execute these steps:
|
||||
1. Create archive/ directory if it doesn't exist
|
||||
2. Generate target name as `YYYY-MM-DD-[change-name]` using current date
|
||||
3. Check if target directory already exists
|
||||
4. Update main specs from the change's future state specs (see Spec Update Process below)
|
||||
5. Move the entire change directory to the archive location
|
||||
|
||||
#### Scenario: Archive already exists
|
||||
|
||||
- **WHEN** target archive already exists
|
||||
- **THEN** fail with error message
|
||||
- **AND** do not overwrite existing archive
|
||||
|
||||
#### Scenario: Successful archive
|
||||
|
||||
- **WHEN** move succeeds
|
||||
- **THEN** display success message with archived name and list of updated specs
|
||||
|
||||
### Requirement: Spec Update Process
|
||||
|
||||
Before moving the change to archive, the command SHALL update main specs to reflect the deployed reality.
|
||||
|
||||
#### Scenario: Updating specs from change
|
||||
|
||||
- **WHEN** the change contains specs in `changes/[name]/specs/`
|
||||
- **THEN** execute these steps:
|
||||
1. Analyze which specs will be affected by comparing with existing specs
|
||||
2. Display a summary of spec updates to the user (see Confirmation Behavior below)
|
||||
3. Prompt for confirmation unless `--yes` flag is provided
|
||||
4. If confirmed, for each capability spec in the change directory:
|
||||
- Copy the spec from `changes/[name]/specs/[capability]/spec.md` to `openspec/specs/[capability]/spec.md`
|
||||
- Create the target directory structure if it doesn't exist
|
||||
- Overwrite existing spec files (specs represent current reality, change specs are the new reality)
|
||||
- Track which specs were updated for the success message
|
||||
|
||||
#### Scenario: No specs in change
|
||||
|
||||
- **WHEN** no specs exist in the change
|
||||
- **THEN** skip the spec update step
|
||||
- **AND** proceed with archiving
|
||||
|
||||
### Requirement: Confirmation Behavior
|
||||
|
||||
The spec update confirmation SHALL provide clear visibility into changes before they are applied.
|
||||
|
||||
#### Scenario: Displaying confirmation
|
||||
|
||||
- **WHEN** prompting for confirmation
|
||||
- **THEN** display a clear summary showing:
|
||||
- Which specs will be created (new capabilities)
|
||||
- Which specs will be updated (existing capabilities)
|
||||
- The source path for each spec
|
||||
- **AND** format the confirmation prompt as:
|
||||
```
|
||||
The following specs will be updated:
|
||||
|
||||
NEW specs to be created:
|
||||
- cli-archive (from changes/add-archive-command/specs/cli-archive/spec.md)
|
||||
|
||||
EXISTING specs to be updated:
|
||||
- cli-init (from changes/update-init-command/specs/cli-init/spec.md)
|
||||
|
||||
Update 2 specs and archive 'add-archive-command'? [y/N]:
|
||||
```
|
||||
#### Scenario: Handling confirmation response
|
||||
|
||||
- **WHEN** waiting for user confirmation
|
||||
- **THEN** default to "No" for safety (require explicit "y" or "yes")
|
||||
- **AND** skip confirmation when `--yes` or `-y` flag is provided
|
||||
|
||||
#### Scenario: User declines confirmation
|
||||
|
||||
- **WHEN** user declines the confirmation
|
||||
- **THEN** abort the entire archive operation
|
||||
- **AND** display message: "Archive cancelled. No changes were made."
|
||||
- **AND** exit with non-zero status code
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Requirement: Error Conditions
|
||||
|
||||
The command SHALL handle various error conditions gracefully.
|
||||
|
||||
#### Scenario: Handling errors
|
||||
|
||||
- **WHEN** errors occur
|
||||
- **THEN** handle the following conditions:
|
||||
- Missing openspec/changes/ directory
|
||||
- Change not found
|
||||
- Archive target already exists
|
||||
- File system permissions issues
|
||||
|
||||
## Why These Decisions
|
||||
|
||||
**Interactive selection**: Reduces typing and helps users see available changes
|
||||
**Task checking**: Prevents accidental archiving of incomplete work
|
||||
**Date prefixing**: Maintains chronological order and prevents naming conflicts
|
||||
**No overwrite**: Preserves historical archives and prevents data loss
|
||||
**Spec updates before archiving**: Specs in the main directory represent current reality; when a change is deployed and archived, its future state specs become the new reality and must replace the main specs
|
||||
**Confirmation for spec updates**: Provides visibility into what will change, prevents accidental overwrites, and ensures users understand the impact before specs are modified
|
||||
**--yes flag for automation**: Allows CI/CD pipelines to archive without interactive prompts while maintaining safety by default for manual use
|
||||
@@ -0,0 +1,120 @@
|
||||
# CLI Diff Command Specification
|
||||
|
||||
## Purpose
|
||||
|
||||
The `openspec diff` command provides developers with a visual comparison between proposed spec changes and the current deployed specs.
|
||||
|
||||
## Command Syntax
|
||||
|
||||
```bash
|
||||
openspec diff [change-name]
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Without Arguments
|
||||
|
||||
The command SHALL provide an interactive selection when no change is specified.
|
||||
|
||||
#### Scenario: Running without arguments
|
||||
|
||||
- **WHEN** running `openspec diff` without arguments
|
||||
- **THEN** list all available changes in the `changes/` directory (excluding archive)
|
||||
- **AND** prompt user to select a change
|
||||
|
||||
### Requirement: With Change Name
|
||||
|
||||
The command SHALL compare specs when a specific change is provided.
|
||||
|
||||
#### Scenario: Running with change name
|
||||
|
||||
- **WHEN** running `openspec diff <change-name>`
|
||||
- **THEN** compare all spec files in `changes/<change-name>/specs/` with corresponding files in `specs/`
|
||||
|
||||
### Requirement: Diff Output
|
||||
|
||||
The command SHALL generate appropriate diff output for all spec changes.
|
||||
|
||||
#### Scenario: Comparing existing files
|
||||
|
||||
- **WHEN** file exists in both locations
|
||||
- **THEN** show unified diff
|
||||
|
||||
#### Scenario: New files
|
||||
|
||||
- **WHEN** file only exists in change
|
||||
- **THEN** show as new file (all lines with +)
|
||||
|
||||
#### Scenario: Deleted files
|
||||
|
||||
- **WHEN** file only exists in current specs
|
||||
- **THEN** show as deleted (all lines with -)
|
||||
|
||||
### Requirement: Display Format
|
||||
|
||||
The command SHALL use standard unified diff format for consistency with existing tools.
|
||||
|
||||
#### Scenario: Formatting diff output
|
||||
|
||||
- **WHEN** displaying diff output
|
||||
- **THEN** use standard unified diff format:
|
||||
- Lines prefixed with `-` for removed content
|
||||
- Lines prefixed with `+` for added content
|
||||
- Lines without prefix for unchanged context
|
||||
- File headers showing the paths being compared
|
||||
|
||||
### Requirement: Color Support
|
||||
|
||||
The command SHALL enhance readability with colors when supported.
|
||||
|
||||
#### Scenario: Terminal with color support
|
||||
|
||||
- **WHEN** terminal supports colors
|
||||
- **THEN** display:
|
||||
- Removed lines in red
|
||||
- Added lines in green
|
||||
- File headers in bold
|
||||
- Context lines in default color
|
||||
|
||||
### Requirement: Error Handling
|
||||
|
||||
The command SHALL provide clear error messages for various failure conditions.
|
||||
|
||||
#### Scenario: Change not found
|
||||
|
||||
- **WHEN** specified change doesn't exist
|
||||
- **THEN** display error "Change '<name>' not found"
|
||||
|
||||
#### Scenario: No specs in change
|
||||
|
||||
- **WHEN** no specs directory in change
|
||||
- **THEN** display "No spec changes found for '<name>'"
|
||||
|
||||
#### Scenario: Missing changes directory
|
||||
|
||||
- **WHEN** changes directory doesn't exist
|
||||
- **THEN** display "No OpenSpec changes directory found"
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# View diff for specific change
|
||||
$ openspec diff add-auth-feature
|
||||
|
||||
--- specs/user-auth/spec.md
|
||||
+++ changes/add-auth-feature/specs/user-auth/spec.md
|
||||
@@ -10,6 +10,8 @@
|
||||
Users SHALL authenticate with email and password.
|
||||
|
||||
+Users MAY authenticate with OAuth providers.
|
||||
+
|
||||
WHEN credentials are valid THEN issue JWT token.
|
||||
|
||||
# List all changes and select
|
||||
$ openspec diff
|
||||
Available changes:
|
||||
1. add-auth-feature
|
||||
2. update-payment-flow
|
||||
3. add-status-command
|
||||
Select a change (1-3):
|
||||
```
|
||||
@@ -0,0 +1,196 @@
|
||||
# CLI Init Specification
|
||||
|
||||
## Purpose
|
||||
|
||||
The `openspec init` command SHALL create a complete OpenSpec directory structure in any project, enabling immediate adoption of OpenSpec conventions with support for multiple AI coding assistants.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Progress Indicators
|
||||
|
||||
The command SHALL display progress indicators during initialization to provide clear feedback about each step.
|
||||
|
||||
#### Scenario: Displaying initialization progress
|
||||
|
||||
- **WHEN** executing initialization steps
|
||||
- **THEN** validate environment silently in background (no output unless error)
|
||||
- **AND** display progress with ora spinners:
|
||||
- Show spinner: "⠋ Creating OpenSpec structure..."
|
||||
- Then success: "✔ OpenSpec structure created"
|
||||
- Show spinner: "⠋ Configuring AI tools..."
|
||||
- Then success: "✔ AI tools configured"
|
||||
|
||||
### Requirement: Directory Creation
|
||||
|
||||
The command SHALL create the complete OpenSpec directory structure with all required directories and files.
|
||||
|
||||
#### Scenario: Creating OpenSpec structure
|
||||
|
||||
- **WHEN** `openspec init` is executed
|
||||
- **THEN** create the following directory structure:
|
||||
```
|
||||
openspec/
|
||||
├── project.md
|
||||
├── README.md
|
||||
├── specs/
|
||||
└── changes/
|
||||
└── archive/
|
||||
```
|
||||
|
||||
### Requirement: File Generation
|
||||
|
||||
The command SHALL generate required template files with appropriate content for immediate use.
|
||||
|
||||
#### Scenario: Generating template files
|
||||
|
||||
- **WHEN** initializing OpenSpec
|
||||
- **THEN** generate `README.md` containing complete OpenSpec instructions for AI assistants
|
||||
- **AND** generate `project.md` with project context template
|
||||
|
||||
### Requirement: AI Tool Configuration
|
||||
|
||||
The command SHALL configure AI coding assistants with OpenSpec instructions based on user selection.
|
||||
|
||||
#### Scenario: Prompting for AI tool selection
|
||||
|
||||
- **WHEN** run interactively
|
||||
- **THEN** prompt user to select AI tools to configure:
|
||||
- Claude Code (updates/creates CLAUDE.md with OpenSpec markers)
|
||||
- Cursor (future)
|
||||
- Aider (future)
|
||||
|
||||
### Requirement: AI Tool Configuration Details
|
||||
|
||||
The command SHALL properly configure selected AI tools with OpenSpec-specific instructions using a marker system.
|
||||
|
||||
#### Scenario: Configuring Claude Code
|
||||
|
||||
- **WHEN** Claude Code is selected
|
||||
- **THEN** create or update `CLAUDE.md` in the project root directory (not inside openspec/)
|
||||
|
||||
#### Scenario: Creating new CLAUDE.md
|
||||
|
||||
- **WHEN** CLAUDE.md does not exist
|
||||
- **THEN** create new file with OpenSpec content wrapped in markers:
|
||||
```markdown
|
||||
<!-- OPENSPEC:START -->
|
||||
# OpenSpec Project
|
||||
|
||||
This document provides instructions for AI coding assistants on how to use OpenSpec conventions for spec-driven development. Follow these rules precisely when working on OpenSpec-enabled projects.
|
||||
|
||||
This project uses OpenSpec for spec-driven development. Specifications are the source of truth.
|
||||
|
||||
See @openspec/README.md for detailed conventions and guidelines.
|
||||
<!-- OPENSPEC:END -->
|
||||
```
|
||||
|
||||
#### Scenario: Updating existing CLAUDE.md
|
||||
|
||||
- **WHEN** CLAUDE.md already exists
|
||||
- **THEN** preserve all existing content
|
||||
- **AND** insert OpenSpec content at the beginning of the file using markers
|
||||
- **AND** ensure markers don't duplicate if they already exist
|
||||
|
||||
#### Scenario: Managing content with markers
|
||||
|
||||
- **WHEN** using the marker system
|
||||
- **THEN** use `<!-- OPENSPEC:START -->` to mark the beginning of managed content
|
||||
- **AND** use `<!-- OPENSPEC:END -->` to mark the end of managed content
|
||||
- **AND** allow OpenSpec to update its content without affecting user customizations
|
||||
- **AND** preserve all content outside the markers intact
|
||||
|
||||
WHY use markers:
|
||||
- Users may have existing CLAUDE.md instructions they want to keep
|
||||
- OpenSpec can update its instructions in future versions
|
||||
- Clear boundary between OpenSpec-managed and user-managed content
|
||||
|
||||
### Requirement: Interactive Mode
|
||||
|
||||
The command SHALL provide an interactive menu for AI tool selection with clear navigation instructions.
|
||||
|
||||
#### Scenario: Displaying interactive menu
|
||||
|
||||
- **WHEN** run
|
||||
- **THEN** prompt user with: "Which AI tool do you use?"
|
||||
- **AND** show single-select menu with available tools:
|
||||
- Claude Code
|
||||
- **AND** show disabled options as "coming soon" (not selectable):
|
||||
- Cursor (coming soon)
|
||||
- Aider (coming soon)
|
||||
- Continue (coming soon)
|
||||
|
||||
#### Scenario: Navigating the menu
|
||||
|
||||
- **WHEN** user is in the menu
|
||||
- **THEN** allow arrow keys to move between options
|
||||
- **AND** allow Enter key to select the highlighted option
|
||||
|
||||
### Requirement: Safety Checks
|
||||
|
||||
The command SHALL perform safety checks to prevent overwriting existing structures and ensure proper permissions.
|
||||
|
||||
#### Scenario: Detecting existing initialization
|
||||
|
||||
- **WHEN** `openspec/` directory already exists
|
||||
- **THEN** display error with ora fail indicator:
|
||||
- "✖ Error: OpenSpec seems to already be initialized. Use 'openspec update' to update the structure."
|
||||
|
||||
#### Scenario: Checking write permissions
|
||||
|
||||
- **WHEN** checking initialization feasibility
|
||||
- **THEN** verify write permissions in the target directory silently
|
||||
- **AND** only display error if permissions are insufficient
|
||||
|
||||
### Requirement: Success Output
|
||||
|
||||
The command SHALL provide clear, actionable next steps upon successful initialization.
|
||||
|
||||
#### Scenario: Displaying success message
|
||||
|
||||
- **WHEN** initialization completes successfully
|
||||
- **THEN** display actionable prompts for AI-driven workflow:
|
||||
```
|
||||
✔ OpenSpec initialized successfully!
|
||||
|
||||
Next steps - Copy these prompts to Claude:
|
||||
|
||||
────────────────────────────────────────────────────────────
|
||||
1. Populate your project context:
|
||||
"Please read openspec/project.md and help me fill it out
|
||||
with details about my project, tech stack, and conventions"
|
||||
|
||||
2. Create your first change proposal:
|
||||
"I want to add [YOUR FEATURE HERE]. Please create an
|
||||
OpenSpec change proposal for this feature"
|
||||
|
||||
3. Learn the OpenSpec workflow:
|
||||
"Please explain the OpenSpec workflow from openspec/README.md
|
||||
and how I should work with you on this project"
|
||||
────────────────────────────────────────────────────────────
|
||||
```
|
||||
|
||||
The prompts SHALL:
|
||||
- Be copy-pasteable for immediate use with AI tools
|
||||
- Guide users through the AI-driven workflow
|
||||
- Replace placeholder text ([YOUR FEATURE HERE]) with actual features
|
||||
|
||||
### Requirement: Exit Codes
|
||||
|
||||
The command SHALL use consistent exit codes to indicate different failure modes.
|
||||
|
||||
#### Scenario: Returning exit codes
|
||||
|
||||
- **WHEN** the command completes
|
||||
- **THEN** return appropriate exit code:
|
||||
- 0: Success
|
||||
- 1: General error (including when OpenSpec directory already exists)
|
||||
- 2: Insufficient permissions (reserved for future use)
|
||||
- 3: User cancelled operation (reserved for future use)
|
||||
|
||||
## Why
|
||||
|
||||
Manual creation of OpenSpec structure is error-prone and creates adoption friction. A standardized init command ensures:
|
||||
- Consistent structure across all projects
|
||||
- Proper AI instruction files are always included
|
||||
- Quick onboarding for new projects
|
||||
- Clear conventions from the start
|
||||
@@ -0,0 +1,96 @@
|
||||
# List Command Specification
|
||||
|
||||
## Purpose
|
||||
|
||||
The `openspec list` command SHALL provide developers with a quick overview of all active changes in the project, showing their names and task completion status.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Command Execution
|
||||
|
||||
The command SHALL scan and analyze all active changes to provide a comprehensive overview.
|
||||
|
||||
#### Scenario: Scanning for changes
|
||||
|
||||
- **WHEN** `openspec list` is executed
|
||||
- **THEN** scan the `openspec/changes/` directory for change directories
|
||||
- **AND** exclude the `archive/` subdirectory from results
|
||||
- **AND** parse each change's `tasks.md` file to count task completion
|
||||
|
||||
### Requirement: Task Counting
|
||||
|
||||
The command SHALL accurately count task completion status using standard markdown checkbox patterns.
|
||||
|
||||
#### Scenario: Counting tasks in tasks.md
|
||||
|
||||
- **WHEN** parsing a `tasks.md` file
|
||||
- **THEN** count tasks matching these patterns:
|
||||
- Completed: Lines containing `- [x]`
|
||||
- Incomplete: Lines containing `- [ ]`
|
||||
- **AND** calculate total tasks as the sum of completed and incomplete
|
||||
|
||||
### Requirement: Output Format
|
||||
|
||||
The command SHALL display changes in a clear, readable table format with progress indicators.
|
||||
|
||||
#### Scenario: Displaying change list
|
||||
|
||||
- **WHEN** displaying the list
|
||||
- **THEN** show a table with columns:
|
||||
- Change name (directory name)
|
||||
- Task progress (e.g., "3/5 tasks" or "✓ Complete")
|
||||
- **AND** use status indicators:
|
||||
- `✓` for fully completed changes (all tasks done)
|
||||
- Progress fraction for partial completion
|
||||
|
||||
Example output:
|
||||
```
|
||||
Changes:
|
||||
add-auth-feature 3/5 tasks
|
||||
update-api-docs ✓ Complete
|
||||
fix-validation 0/2 tasks
|
||||
add-list-command 1/4 tasks
|
||||
```
|
||||
|
||||
### Requirement: Empty State
|
||||
|
||||
The command SHALL provide clear feedback when no active changes are present.
|
||||
|
||||
#### Scenario: Handling empty state
|
||||
|
||||
- **WHEN** no active changes exist (only archive/ or empty changes/)
|
||||
- **THEN** display: "No active changes found."
|
||||
|
||||
### Requirement: Error Handling
|
||||
|
||||
The command SHALL gracefully handle missing files and directories with appropriate messages.
|
||||
|
||||
#### Scenario: Missing tasks.md file
|
||||
|
||||
- **WHEN** a change directory has no `tasks.md` file
|
||||
- **THEN** display the change with "No tasks" status
|
||||
|
||||
#### Scenario: Missing changes directory
|
||||
|
||||
- **WHEN** `openspec/changes/` directory doesn't exist
|
||||
- **THEN** display error: "No OpenSpec changes directory found. Run 'openspec init' first."
|
||||
- **AND** exit with code 1
|
||||
|
||||
### Requirement: Sorting
|
||||
|
||||
The command SHALL maintain consistent ordering of changes for predictable output.
|
||||
|
||||
#### Scenario: Ordering changes
|
||||
|
||||
- **WHEN** displaying multiple changes
|
||||
- **THEN** sort them in alphabetical order by change name
|
||||
|
||||
## Why
|
||||
|
||||
Developers need a quick way to:
|
||||
- See what changes are in progress
|
||||
- Identify which changes are ready to archive
|
||||
- Understand the overall project evolution status
|
||||
- Get a bird's-eye view without opening multiple files
|
||||
|
||||
This command provides that visibility with minimal effort, following OpenSpec's philosophy of simplicity and clarity.
|
||||
@@ -0,0 +1,84 @@
|
||||
# Update Command Specification
|
||||
|
||||
## Purpose
|
||||
|
||||
As a developer using OpenSpec, I want to update the OpenSpec instructions in my project when new versions are released, so that I can benefit from improvements to AI agent instructions.
|
||||
|
||||
## Core Requirements
|
||||
|
||||
### Requirement: Update Behavior
|
||||
|
||||
The update command SHALL update OpenSpec instruction files to the latest templates in a team-friendly manner.
|
||||
|
||||
#### Scenario: Running update command
|
||||
|
||||
- **WHEN** a user runs `openspec update`
|
||||
- **THEN** the command SHALL:
|
||||
- Check if the `openspec` directory exists
|
||||
- Replace `openspec/README.md` with the latest template (complete replacement)
|
||||
- Update **only existing** AI tool configuration files (e.g., CLAUDE.md)
|
||||
- Check each registered AI tool configurator
|
||||
- For each configurator, check if its file exists
|
||||
- Update only files that already exist using their markers
|
||||
- Preserve user content outside markers
|
||||
- **Never create new AI tool configuration files**
|
||||
- Display success message listing updated files
|
||||
|
||||
### Requirement: Prerequisites
|
||||
|
||||
The command SHALL require an existing OpenSpec structure before allowing updates.
|
||||
|
||||
#### Scenario: Checking prerequisites
|
||||
|
||||
- **GIVEN** the command requires an existing `openspec` directory (created by `openspec init`)
|
||||
- **WHEN** the `openspec` directory does not exist
|
||||
- **THEN** display error: "No OpenSpec directory found. Run 'openspec init' first."
|
||||
- **AND** exit with code 1
|
||||
|
||||
### Requirement: File Handling
|
||||
|
||||
The update command SHALL handle file updates in a predictable and safe manner.
|
||||
|
||||
#### Scenario: Updating files
|
||||
|
||||
- **WHEN** updating files
|
||||
- **THEN** completely replace `openspec/README.md` with the latest template
|
||||
- **AND** update only the OpenSpec-managed blocks in **existing** AI tool files using markers
|
||||
- **AND** use the default directory name `openspec`
|
||||
- **AND** be idempotent (repeated runs have no additional effect)
|
||||
- **AND** respect team members' AI tool choices by not creating unwanted files
|
||||
|
||||
## Edge Cases
|
||||
|
||||
### Requirement: Error Handling
|
||||
|
||||
The command SHALL handle edge cases gracefully.
|
||||
|
||||
#### Scenario: File permission errors
|
||||
|
||||
- **WHEN** file write fails
|
||||
- **THEN** let the error bubble up naturally with file path
|
||||
|
||||
#### Scenario: Missing AI tool files
|
||||
|
||||
- **WHEN** an AI tool configuration file doesn't exist
|
||||
- **THEN** skip updating that file
|
||||
- **AND** do not create it
|
||||
|
||||
#### Scenario: Custom directory names
|
||||
|
||||
- **WHEN** considering custom directory names
|
||||
- **THEN** not supported in this change
|
||||
- **AND** the default directory name `openspec` SHALL be used
|
||||
|
||||
## Success Criteria
|
||||
|
||||
Users SHALL be able to:
|
||||
- Update OpenSpec instructions with a single command
|
||||
- Get the latest AI agent instructions
|
||||
- See clear confirmation of the update
|
||||
|
||||
The update process SHALL be:
|
||||
- Simple and fast (no version checking)
|
||||
- Predictable (same result every time)
|
||||
- Self-contained (no network required)
|
||||
@@ -0,0 +1,258 @@
|
||||
# OpenSpec Conventions Specification
|
||||
|
||||
## Purpose
|
||||
|
||||
OpenSpec conventions SHALL define how system capabilities are documented, how changes are proposed and tracked, and how specifications evolve over time. This meta-specification serves as the source of truth for OpenSpec's own conventions.
|
||||
|
||||
## Core Principles
|
||||
|
||||
The system SHALL follow these principles:
|
||||
- Specs reflect what IS currently built and deployed
|
||||
- Changes contain proposals for what SHOULD be changed
|
||||
- AI drives the documentation process
|
||||
- Specs are living documentation kept in sync with deployed code
|
||||
|
||||
## Directory Structure
|
||||
|
||||
### Requirement: Project Structure
|
||||
|
||||
An OpenSpec project SHALL maintain a consistent directory structure for specifications and changes.
|
||||
|
||||
#### Scenario: Initializing project structure
|
||||
|
||||
- **WHEN** an OpenSpec project is initialized
|
||||
- **THEN** it SHALL have this structure:
|
||||
```
|
||||
openspec/
|
||||
├── project.md # Project-specific context
|
||||
├── README.md # AI assistant instructions
|
||||
├── specs/ # Current deployed capabilities
|
||||
│ └── [capability]/ # Single, focused capability
|
||||
│ ├── spec.md # WHAT and WHY
|
||||
│ └── design.md # HOW (optional, for established patterns)
|
||||
└── changes/ # Proposed changes
|
||||
├── [change-name]/ # Descriptive change identifier
|
||||
│ ├── proposal.md # Why, what, and impact
|
||||
│ ├── tasks.md # Implementation checklist
|
||||
│ ├── design.md # Technical decisions (optional)
|
||||
│ └── specs/ # Complete future state
|
||||
│ └── [capability]/
|
||||
│ └── spec.md # Clean markdown (no diff syntax)
|
||||
└── archive/ # Completed changes
|
||||
└── YYYY-MM-DD-[name]/
|
||||
```
|
||||
|
||||
## Specification Format
|
||||
|
||||
### Requirement: Structured Format for Behavioral Specs
|
||||
|
||||
Behavioral specifications SHALL use a structured format with consistent section headers and keywords to ensure visual consistency and parseability.
|
||||
|
||||
#### Scenario: Writing requirement sections
|
||||
|
||||
- **WHEN** documenting a requirement in a behavioral specification
|
||||
- **THEN** use a level-3 heading with format `### Requirement: [Name]`
|
||||
- **AND** immediately follow with a SHALL statement describing core behavior
|
||||
- **AND** keep requirement names descriptive and under 50 characters
|
||||
|
||||
#### Scenario: Documenting scenarios
|
||||
|
||||
- **WHEN** documenting specific behaviors or use cases
|
||||
- **THEN** use level-4 headings with format `#### Scenario: [Description]`
|
||||
- **AND** use bullet points with bold keywords for steps:
|
||||
- **GIVEN** for initial state (optional)
|
||||
- **WHEN** for conditions or triggers
|
||||
- **THEN** for expected outcomes
|
||||
- **AND** for additional outcomes or conditions
|
||||
|
||||
#### Scenario: Adding implementation details
|
||||
|
||||
- **WHEN** a step requires additional detail
|
||||
- **THEN** use sub-bullets under the main step
|
||||
- **AND** maintain consistent indentation
|
||||
- Sub-bullets provide examples or specifics
|
||||
- Keep sub-bullets concise
|
||||
|
||||
|
||||
## Change Storage Convention
|
||||
|
||||
### Requirement: Header-Based Requirement Identification
|
||||
|
||||
Requirement headers SHALL serve as unique identifiers for programmatic matching between current specs and proposed changes.
|
||||
|
||||
#### Scenario: Matching requirements programmatically
|
||||
|
||||
- **WHEN** processing delta changes
|
||||
- **THEN** use the `### Requirement: [Name]` header as the unique identifier
|
||||
- **AND** match using normalized headers: `normalize(header) = trim(header)`
|
||||
- **AND** compare headers with case-sensitive equality after normalization
|
||||
|
||||
#### Scenario: Handling requirement renames
|
||||
|
||||
- **WHEN** renaming a requirement
|
||||
- **THEN** use a special `## RENAMED Requirements` section
|
||||
- **AND** specify both old and new names explicitly:
|
||||
```markdown
|
||||
## RENAMED Requirements
|
||||
- FROM: `### Requirement: Old Name`
|
||||
- TO: `### Requirement: New Name`
|
||||
```
|
||||
- **AND** if content also changes, include under MODIFIED using the NEW header
|
||||
|
||||
#### Scenario: Validating header uniqueness
|
||||
|
||||
- **WHEN** creating or modifying requirements
|
||||
- **THEN** ensure no duplicate headers exist within a spec
|
||||
- **AND** validation tools SHALL flag duplicate headers as errors
|
||||
|
||||
### Requirement: Change Storage Convention
|
||||
|
||||
Change proposals SHALL store only the additions, modifications, and removals to specifications, not complete future states.
|
||||
|
||||
#### Scenario: Creating change proposals with additions
|
||||
|
||||
- **WHEN** creating a change proposal that adds new requirements
|
||||
- **THEN** include only the new requirements under `## ADDED Requirements`
|
||||
- **AND** each requirement SHALL include its complete content
|
||||
- **AND** use the standard structured format for requirements and scenarios
|
||||
|
||||
#### Scenario: Creating change proposals with modifications
|
||||
|
||||
- **WHEN** creating a change proposal that modifies existing requirements
|
||||
- **THEN** include the modified requirements under `## MODIFIED Requirements`
|
||||
- **AND** use the same header text as in the current spec (normalized)
|
||||
- **AND** include the complete modified requirement (not a diff)
|
||||
- **AND** optionally annotate what changed with inline comments like `← (was X)`
|
||||
|
||||
#### Scenario: Creating change proposals with removals
|
||||
|
||||
- **WHEN** creating a change proposal that removes requirements
|
||||
- **THEN** list them under `## REMOVED Requirements`
|
||||
- **AND** use the normalized header text for identification
|
||||
- **AND** include reason for removal
|
||||
- **AND** document any migration path if applicable
|
||||
|
||||
|
||||
The `changes/[name]/specs/` directory SHALL contain:
|
||||
- Delta files showing only what changes
|
||||
- Sections for ADDED, MODIFIED, REMOVED, and RENAMED requirements
|
||||
- Normalized header matching for requirement identification
|
||||
- Complete requirements using the structured format
|
||||
- Clear indication of change type for each requirement
|
||||
|
||||
#### Scenario: Using standard output symbols
|
||||
|
||||
- **WHEN** displaying delta operations in CLI output
|
||||
- **THEN** use these standard symbols:
|
||||
- `+` for ADDED (green)
|
||||
- `~` for MODIFIED (yellow)
|
||||
- `-` for REMOVED (red)
|
||||
- `→` for RENAMED (cyan)
|
||||
|
||||
### Requirement: Archive Process Enhancement
|
||||
|
||||
The archive process SHALL programmatically apply delta changes to current specifications using header-based matching.
|
||||
|
||||
#### Scenario: Archiving changes with deltas
|
||||
|
||||
- **WHEN** archiving a completed change
|
||||
- **THEN** the archive command SHALL:
|
||||
1. Parse RENAMED sections first and apply renames
|
||||
2. Parse REMOVED sections and remove by normalized header match
|
||||
3. Parse MODIFIED sections and replace by normalized header match (using new names if renamed)
|
||||
4. Parse ADDED sections and append new requirements
|
||||
- **AND** validate that all MODIFIED/REMOVED headers exist in current spec
|
||||
- **AND** validate that ADDED headers don't already exist
|
||||
- **AND** generate the updated spec in the main specs/ directory
|
||||
|
||||
#### Scenario: Handling conflicts during archive
|
||||
|
||||
- **WHEN** delta changes conflict with current spec state
|
||||
- **THEN** the archive command SHALL report specific conflicts
|
||||
- **AND** require manual resolution before proceeding
|
||||
- **AND** provide clear guidance on resolving conflicts
|
||||
|
||||
### Requirement: Proposal Format
|
||||
|
||||
Proposals SHALL explicitly document all changes with clear from/to comparisons.
|
||||
|
||||
#### Scenario: Documenting changes
|
||||
|
||||
- **WHEN** documenting what changes
|
||||
- **THEN** the proposal SHALL explicitly describe each change:
|
||||
|
||||
```markdown
|
||||
**[Section or Behavior Name]**
|
||||
- From: [current state/requirement]
|
||||
- To: [future state/requirement]
|
||||
- Reason: [why this change is needed]
|
||||
- Impact: [breaking/non-breaking, who's affected]
|
||||
```
|
||||
|
||||
This explicit format compensates for not having inline diffs and ensures reviewers understand exactly what will change.
|
||||
|
||||
## Change Lifecycle
|
||||
|
||||
The change process SHALL follow these states:
|
||||
|
||||
1. **Propose**: AI creates change with future state specs and explicit proposal
|
||||
2. **Review**: Humans review proposal and future state
|
||||
3. **Approve**: Change is approved for implementation
|
||||
4. **Implement**: Follow tasks.md checklist (can span multiple PRs)
|
||||
5. **Deploy**: Changes are deployed to production
|
||||
6. **Update**: Specs in `specs/` are updated to match deployed reality
|
||||
7. **Archive**: Change is moved to `archive/YYYY-MM-DD-[name]/`
|
||||
|
||||
## Viewing Changes
|
||||
|
||||
### Requirement: Change Review
|
||||
|
||||
The system SHALL support multiple methods for reviewing proposed changes.
|
||||
|
||||
#### Scenario: Reviewing changes
|
||||
|
||||
- **WHEN** reviewing proposed changes
|
||||
- **THEN** reviewers can compare using:
|
||||
- GitHub PR diff view when changes are committed
|
||||
- Command line: `diff -u specs/[capability]/spec.md changes/[name]/specs/[capability]/spec.md`
|
||||
- Any visual diff tool comparing current vs future state
|
||||
|
||||
The system relies on tools to generate diffs rather than storing them.
|
||||
|
||||
## Capability Naming
|
||||
|
||||
Capabilities SHALL use:
|
||||
- Verb-noun patterns (e.g., `user-auth`, `payment-capture`)
|
||||
- Hyphenated lowercase names
|
||||
- Singular focus (one responsibility per capability)
|
||||
- No nesting (flat structure under `specs/`)
|
||||
|
||||
## When Changes Require Proposals
|
||||
|
||||
A proposal SHALL be created for:
|
||||
- New features or capabilities
|
||||
- Breaking changes to existing behavior
|
||||
- Architecture or pattern changes
|
||||
- Performance optimizations that change behavior
|
||||
- Security updates affecting access patterns
|
||||
|
||||
A proposal is NOT required for:
|
||||
- Bug fixes restoring intended behavior
|
||||
- Typos or formatting fixes
|
||||
- Non-breaking dependency updates
|
||||
- Adding tests for existing behavior
|
||||
- Documentation clarifications
|
||||
|
||||
## Why This Approach
|
||||
|
||||
Clean future state storage provides:
|
||||
- **Readability**: No diff syntax pollution
|
||||
- **AI-compatibility**: Standard markdown that AI tools understand
|
||||
- **Simplicity**: No special parsing or processing needed
|
||||
- **Tool-agnostic**: Any diff tool can show changes
|
||||
- **Clear intent**: Explicit proposals document reasoning
|
||||
|
||||
The structured format adds:
|
||||
- **Visual Consistency**: Requirement and Scenario prefixes make sections instantly recognizable
|
||||
- **Parseability**: Consistent structure enables tooling and automation
|
||||
- **Gradual Adoption**: Existing specs can migrate incrementally
|
||||
+12
-2
@@ -37,6 +37,10 @@
|
||||
"build": "node build.js",
|
||||
"dev": "tsc --watch",
|
||||
"dev:cli": "pnpm build && node bin/openspec.js",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:ui": "vitest --ui",
|
||||
"test:coverage": "vitest --coverage",
|
||||
"prepare": "npm run build"
|
||||
},
|
||||
"engines": {
|
||||
@@ -44,10 +48,16 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.2.0",
|
||||
"typescript": "^5.9.2"
|
||||
"@vitest/ui": "^3.2.4",
|
||||
"typescript": "^5.9.2",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@inquirer/prompts": "^7.8.0",
|
||||
"commander": "^14.0.0"
|
||||
"chalk": "^5.5.0",
|
||||
"commander": "^14.0.0",
|
||||
"jest-diff": "^30.0.5",
|
||||
"ora": "^8.2.0",
|
||||
"zod": "^4.0.17"
|
||||
}
|
||||
}
|
||||
Generated
+1206
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,16 @@
|
||||
import { Command } from 'commander';
|
||||
import ora from 'ora';
|
||||
import path from 'path';
|
||||
import { promises as fs } from 'fs';
|
||||
import { InitCommand } from '../core/init.js';
|
||||
import { UpdateCommand } from '../core/update.js';
|
||||
import { DiffCommand } from '../core/diff.js';
|
||||
import { ListCommand } from '../core/list.js';
|
||||
import { ArchiveCommand } from '../core/archive.js';
|
||||
import { registerSpecCommand } from '../commands/spec.js';
|
||||
import { ChangeCommand } from '../commands/change.js';
|
||||
import { ValidateCommand } from '../commands/validate.js';
|
||||
import { ShowCommand } from '../commands/show.js';
|
||||
|
||||
const program = new Command();
|
||||
|
||||
@@ -7,4 +19,214 @@ program
|
||||
.description('AI-native system for spec-driven development')
|
||||
.version('0.0.1');
|
||||
|
||||
// Global options
|
||||
program.option('--no-color', 'Disable color output');
|
||||
|
||||
// Apply global flags before any command runs
|
||||
program.hook('preAction', (thisCommand) => {
|
||||
const opts = thisCommand.opts();
|
||||
if (opts.noColor) {
|
||||
process.env.NO_COLOR = '1';
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command('init [path]')
|
||||
.description('Initialize OpenSpec in your project')
|
||||
.action(async (targetPath = '.') => {
|
||||
try {
|
||||
// Validate that the path is a valid directory
|
||||
const resolvedPath = path.resolve(targetPath);
|
||||
|
||||
try {
|
||||
const stats = await fs.stat(resolvedPath);
|
||||
if (!stats.isDirectory()) {
|
||||
throw new Error(`Path "${targetPath}" is not a directory`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.code === 'ENOENT') {
|
||||
// Directory doesn't exist, but we can create it
|
||||
console.log(`Directory "${targetPath}" doesn't exist, it will be created.`);
|
||||
} else if (error.message && error.message.includes('not a directory')) {
|
||||
throw error;
|
||||
} else {
|
||||
throw new Error(`Cannot access path "${targetPath}": ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const initCommand = new InitCommand();
|
||||
await initCommand.execute(targetPath);
|
||||
} catch (error) {
|
||||
console.log(); // Empty line for spacing
|
||||
ora().fail(`Error: ${(error as Error).message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command('update [path]')
|
||||
.description('Update OpenSpec instruction files')
|
||||
.action(async (targetPath = '.') => {
|
||||
try {
|
||||
const resolvedPath = path.resolve(targetPath);
|
||||
const updateCommand = new UpdateCommand();
|
||||
await updateCommand.execute(resolvedPath);
|
||||
} catch (error) {
|
||||
console.log(); // Empty line for spacing
|
||||
ora().fail(`Error: ${(error as Error).message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command('diff [change-name]')
|
||||
.description('Show differences between proposed spec changes and current specs (includes validation warnings)')
|
||||
.action(async (changeName?: string) => {
|
||||
try {
|
||||
const diffCommand = new DiffCommand();
|
||||
await diffCommand.execute(changeName);
|
||||
} catch (error) {
|
||||
console.log(); // Empty line for spacing
|
||||
ora().fail(`Error: ${(error as Error).message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command('list')
|
||||
.description('List all active changes with their task status (DEPRECATED: use "openspec change list" instead)')
|
||||
.action(async () => {
|
||||
try {
|
||||
console.log('\x1b[33m%s\x1b[0m', 'Warning: The "openspec list" command is deprecated. Please use "openspec change list" instead.\n');
|
||||
const listCommand = new ListCommand();
|
||||
await listCommand.execute();
|
||||
} catch (error) {
|
||||
console.log(); // Empty line for spacing
|
||||
ora().fail(`Error: ${(error as Error).message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
// Change command with subcommands
|
||||
const changeCmd = program
|
||||
.command('change')
|
||||
.description('Manage OpenSpec change proposals');
|
||||
|
||||
changeCmd
|
||||
.command('show [change-name]')
|
||||
.description('Show a change proposal in JSON or markdown format')
|
||||
.option('--json', 'Output as JSON')
|
||||
.option('--deltas-only', 'Show only deltas (JSON only)')
|
||||
.option('--requirements-only', 'Alias for --deltas-only (deprecated)')
|
||||
.option('--no-interactive', 'Disable interactive prompts')
|
||||
.action(async (changeName?: string, options?: { json?: boolean; requirementsOnly?: boolean; deltasOnly?: boolean; noInteractive?: boolean }) => {
|
||||
try {
|
||||
const changeCommand = new ChangeCommand();
|
||||
await changeCommand.show(changeName, options);
|
||||
} catch (error) {
|
||||
console.error(`Error: ${(error as Error).message}`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
});
|
||||
|
||||
changeCmd
|
||||
.command('list')
|
||||
.description('List all active changes')
|
||||
.option('--json', 'Output as JSON')
|
||||
.option('--long', 'Show id and title with counts')
|
||||
.action(async (options?: { json?: boolean; long?: boolean }) => {
|
||||
try {
|
||||
const changeCommand = new ChangeCommand();
|
||||
await changeCommand.list(options);
|
||||
} catch (error) {
|
||||
console.error(`Error: ${(error as Error).message}`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
});
|
||||
|
||||
changeCmd
|
||||
.command('validate [change-name]')
|
||||
.description('Validate a change proposal')
|
||||
.option('--strict', 'Enable strict validation mode')
|
||||
.option('--json', 'Output validation report as JSON')
|
||||
.option('--no-interactive', 'Disable interactive prompts')
|
||||
.action(async (changeName?: string, options?: { strict?: boolean; json?: boolean; noInteractive?: boolean }) => {
|
||||
try {
|
||||
const changeCommand = new ChangeCommand();
|
||||
await changeCommand.validate(changeName, options);
|
||||
} catch (error) {
|
||||
console.error(`Error: ${(error as Error).message}`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command('archive [change-name]')
|
||||
.description('Archive a completed change and update main specs')
|
||||
.option('-y, --yes', 'Skip confirmation prompts')
|
||||
.option('--skip-specs', 'Skip spec update operations (useful for infrastructure, tooling, or doc-only changes)')
|
||||
.option('--no-validate', 'Skip validation (not recommended, requires confirmation)')
|
||||
.action(async (changeName?: string, options?: { yes?: boolean; skipSpecs?: boolean; noValidate?: boolean }) => {
|
||||
try {
|
||||
const archiveCommand = new ArchiveCommand();
|
||||
await archiveCommand.execute(changeName, options);
|
||||
} catch (error) {
|
||||
console.log(); // Empty line for spacing
|
||||
ora().fail(`Error: ${(error as Error).message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
registerSpecCommand(program);
|
||||
|
||||
// Top-level validate command
|
||||
program
|
||||
.command('validate [item-name]')
|
||||
.description('Validate changes and specs')
|
||||
.option('--all', 'Validate all changes and specs')
|
||||
.option('--changes', 'Validate all changes')
|
||||
.option('--specs', 'Validate all specs')
|
||||
.option('--type <type>', 'Specify item type when ambiguous: change|spec')
|
||||
.option('--strict', 'Enable strict validation mode')
|
||||
.option('--json', 'Output validation results as JSON')
|
||||
.option('--concurrency <n>', 'Max concurrent validations (defaults to env OPENSPEC_CONCURRENCY or 6)')
|
||||
.option('--no-interactive', 'Disable interactive prompts')
|
||||
.action(async (itemName?: string, options?: { all?: boolean; changes?: boolean; specs?: boolean; type?: string; strict?: boolean; json?: boolean; noInteractive?: boolean; concurrency?: string }) => {
|
||||
try {
|
||||
const validateCommand = new ValidateCommand();
|
||||
await validateCommand.execute(itemName, options);
|
||||
} catch (error) {
|
||||
console.log();
|
||||
ora().fail(`Error: ${(error as Error).message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
// Top-level show command
|
||||
program
|
||||
.command('show [item-name]')
|
||||
.description('Show a change or spec')
|
||||
.option('--json', 'Output as JSON')
|
||||
.option('--type <type>', 'Specify item type when ambiguous: change|spec')
|
||||
.option('--no-interactive', 'Disable interactive prompts')
|
||||
// change-only flags
|
||||
.option('--deltas-only', 'Show only deltas (JSON only, change)')
|
||||
.option('--requirements-only', 'Alias for --deltas-only (deprecated, change)')
|
||||
// spec-only flags
|
||||
.option('--requirements', 'JSON only: Show only requirements (exclude scenarios)')
|
||||
.option('--no-scenarios', 'JSON only: Exclude scenario content')
|
||||
.option('-r, --requirement <id>', 'JSON only: Show specific requirement by ID (1-based)')
|
||||
// allow unknown options to pass-through to underlying command implementation
|
||||
.allowUnknownOption(true)
|
||||
.action(async (itemName?: string, options?: { json?: boolean; type?: string; noInteractive?: boolean; [k: string]: any }) => {
|
||||
try {
|
||||
const showCommand = new ShowCommand();
|
||||
await showCommand.execute(itemName, options ?? {});
|
||||
} catch (error) {
|
||||
console.log();
|
||||
ora().fail(`Error: ${(error as Error).message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
program.parse();
|
||||
@@ -0,0 +1,269 @@
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { select } from '@inquirer/prompts';
|
||||
import { JsonConverter } from '../core/converters/json-converter.js';
|
||||
import { Validator } from '../core/validation/validator.js';
|
||||
import { ChangeParser } from '../core/parsers/change-parser.js';
|
||||
import { Change } from '../core/schemas/index.js';
|
||||
import { isInteractive } from '../utils/interactive.js';
|
||||
import { getActiveChangeIds } from '../utils/item-discovery.js';
|
||||
|
||||
// Constants for better maintainability
|
||||
const ARCHIVE_DIR = 'archive';
|
||||
const TASK_PATTERN = /^[-*]\s+\[[\sx]\]/i;
|
||||
const COMPLETED_TASK_PATTERN = /^[-*]\s+\[x\]/i;
|
||||
|
||||
export class ChangeCommand {
|
||||
private converter: JsonConverter;
|
||||
|
||||
constructor() {
|
||||
this.converter = new JsonConverter();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a change proposal.
|
||||
* - Text mode: raw markdown passthrough (no filters)
|
||||
* - JSON mode: minimal object with deltas; --deltas-only returns same object with filtered deltas
|
||||
* Note: --requirements-only is deprecated alias for --deltas-only
|
||||
*/
|
||||
async show(changeName?: string, options?: { json?: boolean; requirementsOnly?: boolean; deltasOnly?: boolean; noInteractive?: boolean }): Promise<void> {
|
||||
const changesPath = path.join(process.cwd(), 'openspec', 'changes');
|
||||
|
||||
if (!changeName) {
|
||||
const canPrompt = isInteractive(options?.noInteractive);
|
||||
const changes = await this.getActiveChanges(changesPath);
|
||||
if (canPrompt && changes.length > 0) {
|
||||
const selected = await select({
|
||||
message: 'Select a change to show',
|
||||
choices: changes.map(id => ({ name: id, value: id })),
|
||||
});
|
||||
changeName = selected;
|
||||
} else {
|
||||
if (changes.length === 0) {
|
||||
console.error('No change specified. No active changes found.');
|
||||
} else {
|
||||
console.error(`No change specified. Available IDs: ${changes.join(', ')}`);
|
||||
}
|
||||
console.error('Hint: use "openspec change list" to view available changes.');
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const proposalPath = path.join(changesPath, changeName, 'proposal.md');
|
||||
|
||||
try {
|
||||
await fs.access(proposalPath);
|
||||
} catch {
|
||||
throw new Error(`Change "${changeName}" not found at ${proposalPath}`);
|
||||
}
|
||||
|
||||
if (options?.json) {
|
||||
const jsonOutput = await this.converter.convertChangeToJson(proposalPath);
|
||||
|
||||
if (options.requirementsOnly) {
|
||||
console.error('Flag --requirements-only is deprecated; use --deltas-only instead.');
|
||||
}
|
||||
|
||||
const parsed: Change = JSON.parse(jsonOutput);
|
||||
const contentForTitle = await fs.readFile(proposalPath, 'utf-8');
|
||||
const title = this.extractTitle(contentForTitle);
|
||||
const id = parsed.name;
|
||||
const deltas = parsed.deltas || [];
|
||||
|
||||
if (options.requirementsOnly || options.deltasOnly) {
|
||||
const output = { id, title, deltaCount: deltas.length, deltas };
|
||||
console.log(JSON.stringify(output, null, 2));
|
||||
} else {
|
||||
const output = {
|
||||
id,
|
||||
title,
|
||||
deltaCount: deltas.length,
|
||||
deltas,
|
||||
};
|
||||
console.log(JSON.stringify(output, null, 2));
|
||||
}
|
||||
} else {
|
||||
const content = await fs.readFile(proposalPath, 'utf-8');
|
||||
console.log(content);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List active changes.
|
||||
* - Text default: IDs only; --long prints minimal details (title, counts)
|
||||
* - JSON: array of { id, title, deltaCount, taskStatus }, sorted by id
|
||||
*/
|
||||
async list(options?: { json?: boolean; long?: boolean }): Promise<void> {
|
||||
const changesPath = path.join(process.cwd(), 'openspec', 'changes');
|
||||
|
||||
const changes = await this.getActiveChanges(changesPath);
|
||||
|
||||
if (options?.json) {
|
||||
const changeDetails = await Promise.all(
|
||||
changes.map(async (changeName) => {
|
||||
const proposalPath = path.join(changesPath, changeName, 'proposal.md');
|
||||
const tasksPath = path.join(changesPath, changeName, 'tasks.md');
|
||||
|
||||
try {
|
||||
const content = await fs.readFile(proposalPath, 'utf-8');
|
||||
const changeDir = path.join(changesPath, changeName);
|
||||
const parser = new ChangeParser(content, changeDir);
|
||||
const change = await parser.parseChangeWithDeltas(changeName);
|
||||
|
||||
let taskStatus = { total: 0, completed: 0 };
|
||||
try {
|
||||
const tasksContent = await fs.readFile(tasksPath, 'utf-8');
|
||||
taskStatus = this.countTasks(tasksContent);
|
||||
} catch (error) {
|
||||
// Tasks file may not exist, which is okay
|
||||
if (process.env.DEBUG) {
|
||||
console.error(`Failed to read tasks file at ${tasksPath}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: changeName,
|
||||
title: this.extractTitle(content),
|
||||
deltaCount: change.deltas.length,
|
||||
taskStatus,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
id: changeName,
|
||||
title: 'Unknown',
|
||||
deltaCount: 0,
|
||||
taskStatus: { total: 0, completed: 0 },
|
||||
};
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const sorted = changeDetails.sort((a, b) => a.id.localeCompare(b.id));
|
||||
console.log(JSON.stringify(sorted, null, 2));
|
||||
} else {
|
||||
if (changes.length === 0) {
|
||||
console.log('No items found');
|
||||
return;
|
||||
}
|
||||
const sorted = [...changes].sort();
|
||||
if (!options?.long) {
|
||||
// IDs only
|
||||
sorted.forEach(id => console.log(id));
|
||||
return;
|
||||
}
|
||||
|
||||
// Long format: id: title and minimal counts
|
||||
for (const changeName of sorted) {
|
||||
const proposalPath = path.join(changesPath, changeName, 'proposal.md');
|
||||
const tasksPath = path.join(changesPath, changeName, 'tasks.md');
|
||||
try {
|
||||
const content = await fs.readFile(proposalPath, 'utf-8');
|
||||
const title = this.extractTitle(content);
|
||||
let taskStatusText = '';
|
||||
try {
|
||||
const tasksContent = await fs.readFile(tasksPath, 'utf-8');
|
||||
const { total, completed } = this.countTasks(tasksContent);
|
||||
taskStatusText = ` [tasks ${completed}/${total}]`;
|
||||
} catch (error) {
|
||||
if (process.env.DEBUG) {
|
||||
console.error(`Failed to read tasks file at ${tasksPath}:`, error);
|
||||
}
|
||||
}
|
||||
const changeDir = path.join(changesPath, changeName);
|
||||
const parser = new ChangeParser(await fs.readFile(proposalPath, 'utf-8'), changeDir);
|
||||
const change = await parser.parseChangeWithDeltas(changeName);
|
||||
const deltaCountText = ` [deltas ${change.deltas.length}]`;
|
||||
console.log(`${changeName}: ${title}${deltaCountText}${taskStatusText}`);
|
||||
} catch {
|
||||
console.log(`${changeName}: (unable to read)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async validate(changeName?: string, options?: { strict?: boolean; json?: boolean; noInteractive?: boolean }): Promise<void> {
|
||||
const changesPath = path.join(process.cwd(), 'openspec', 'changes');
|
||||
|
||||
if (!changeName) {
|
||||
const canPrompt = isInteractive(options?.noInteractive);
|
||||
const changes = await getActiveChangeIds();
|
||||
if (canPrompt && changes.length > 0) {
|
||||
const selected = await select({
|
||||
message: 'Select a change to validate',
|
||||
choices: changes.map(id => ({ name: id, value: id })),
|
||||
});
|
||||
changeName = selected;
|
||||
} else {
|
||||
if (changes.length === 0) {
|
||||
console.error('No change specified. No active changes found.');
|
||||
} else {
|
||||
console.error(`No change specified. Available IDs: ${changes.join(', ')}`);
|
||||
}
|
||||
console.error('Hint: use "openspec change list" to view available changes.');
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const proposalPath = path.join(changesPath, changeName, 'proposal.md');
|
||||
|
||||
try {
|
||||
await fs.access(proposalPath);
|
||||
} catch {
|
||||
throw new Error(`Change "${changeName}" not found at ${proposalPath}`);
|
||||
}
|
||||
|
||||
const validator = new Validator(options?.strict || false);
|
||||
const report = await validator.validateChange(proposalPath);
|
||||
|
||||
if (options?.json) {
|
||||
console.log(JSON.stringify(report, null, 2));
|
||||
} else {
|
||||
if (report.valid) {
|
||||
console.log(`Change "${changeName}" is valid`);
|
||||
} else {
|
||||
console.error(`Change "${changeName}" has validation issues`);
|
||||
report.issues.forEach(issue => {
|
||||
const label = issue.level === 'ERROR' ? 'ERROR' : 'WARNING';
|
||||
const prefix = issue.level === 'ERROR' ? '✗' : '⚠';
|
||||
console.error(`${prefix} [${label}] ${issue.path}: ${issue.message}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async getActiveChanges(changesPath: string): Promise<string[]> {
|
||||
try {
|
||||
const entries = await fs.readdir(changesPath, { withFileTypes: true });
|
||||
return entries
|
||||
.filter(entry => entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== ARCHIVE_DIR)
|
||||
.map(entry => entry.name)
|
||||
.sort();
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private extractTitle(content: string): string {
|
||||
const match = content.match(/^#\s+(?:Change:\s+)?(.+)$/m);
|
||||
return match ? match[1].trim() : 'Untitled Change';
|
||||
}
|
||||
|
||||
private countTasks(content: string): { total: number; completed: number } {
|
||||
const lines = content.split('\n');
|
||||
let total = 0;
|
||||
let completed = 0;
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.match(TASK_PATTERN)) {
|
||||
total++;
|
||||
if (line.match(COMPLETED_TASK_PATTERN)) {
|
||||
completed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { total, completed };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import { select } from '@inquirer/prompts';
|
||||
import path from 'path';
|
||||
import { isInteractive } from '../utils/interactive.js';
|
||||
import { getActiveChangeIds, getSpecIds } from '../utils/item-discovery.js';
|
||||
import { ChangeCommand } from './change.js';
|
||||
import { SpecCommand } from './spec.js';
|
||||
import { nearestMatches } from '../utils/match.js';
|
||||
|
||||
type ItemType = 'change' | 'spec';
|
||||
|
||||
const CHANGE_FLAG_KEYS = new Set(['deltasOnly', 'requirementsOnly']);
|
||||
const SPEC_FLAG_KEYS = new Set(['requirements', 'scenarios', 'requirement']);
|
||||
|
||||
export class ShowCommand {
|
||||
async execute(itemName?: string, options: { json?: boolean; type?: string; noInteractive?: boolean; [k: string]: any } = {}): Promise<void> {
|
||||
const interactive = isInteractive(options.noInteractive);
|
||||
const typeOverride = this.normalizeType(options.type);
|
||||
|
||||
if (!itemName) {
|
||||
if (interactive) {
|
||||
const type = await select<ItemType>({
|
||||
message: 'What would you like to show?',
|
||||
choices: [
|
||||
{ name: 'Change', value: 'change' as const },
|
||||
{ name: 'Spec', value: 'spec' as const },
|
||||
],
|
||||
});
|
||||
await this.runInteractiveByType(type, options);
|
||||
return;
|
||||
}
|
||||
this.printNonInteractiveHint();
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
await this.showDirect(itemName, { typeOverride, options });
|
||||
}
|
||||
|
||||
private normalizeType(value?: string): ItemType | undefined {
|
||||
if (!value) return undefined;
|
||||
const v = value.toLowerCase();
|
||||
if (v === 'change' || v === 'spec') return v;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private async runInteractiveByType(type: ItemType, options: { json?: boolean; noInteractive?: boolean; [k: string]: any }): Promise<void> {
|
||||
if (type === 'change') {
|
||||
const changes = await getActiveChangeIds();
|
||||
if (changes.length === 0) {
|
||||
console.error('No changes found.');
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
const picked = await select<string>({ message: 'Pick a change', choices: changes.map(id => ({ name: id, value: id })) });
|
||||
const cmd = new ChangeCommand();
|
||||
await cmd.show(picked, options as any);
|
||||
return;
|
||||
}
|
||||
|
||||
const specs = await getSpecIds();
|
||||
if (specs.length === 0) {
|
||||
console.error('No specs found.');
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
const picked = await select<string>({ message: 'Pick a spec', choices: specs.map(id => ({ name: id, value: id })) });
|
||||
const cmd = new SpecCommand();
|
||||
await cmd.show(picked, options as any);
|
||||
}
|
||||
|
||||
private async showDirect(itemName: string, params: { typeOverride?: ItemType; options: { json?: boolean; [k: string]: any } }): Promise<void> {
|
||||
// Optimize lookups when type is pre-specified
|
||||
let isChange = false;
|
||||
let isSpec = false;
|
||||
let changes: string[] = [];
|
||||
let specs: string[] = [];
|
||||
if (params.typeOverride === 'change') {
|
||||
changes = await getActiveChangeIds();
|
||||
isChange = changes.includes(itemName);
|
||||
} else if (params.typeOverride === 'spec') {
|
||||
specs = await getSpecIds();
|
||||
isSpec = specs.includes(itemName);
|
||||
} else {
|
||||
[changes, specs] = await Promise.all([getActiveChangeIds(), getSpecIds()]);
|
||||
isChange = changes.includes(itemName);
|
||||
isSpec = specs.includes(itemName);
|
||||
}
|
||||
|
||||
const resolvedType = params.typeOverride ?? (isChange ? 'change' : isSpec ? 'spec' : undefined);
|
||||
|
||||
if (!resolvedType) {
|
||||
console.error(`Unknown item '${itemName}'`);
|
||||
const suggestions = nearestMatches(itemName, [...changes, ...specs]);
|
||||
if (suggestions.length) console.error(`Did you mean: ${suggestions.join(', ')}?`);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!params.typeOverride && isChange && isSpec) {
|
||||
console.error(`Ambiguous item '${itemName}' matches both a change and a spec.`);
|
||||
console.error('Pass --type change|spec, or use: openspec change show / openspec spec show');
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
this.warnIrrelevantFlags(resolvedType, params.options);
|
||||
if (resolvedType === 'change') {
|
||||
const cmd = new ChangeCommand();
|
||||
await cmd.show(itemName, params.options as any);
|
||||
return;
|
||||
}
|
||||
const cmd = new SpecCommand();
|
||||
await cmd.show(itemName, params.options as any);
|
||||
}
|
||||
|
||||
private printNonInteractiveHint(): void {
|
||||
console.error('Nothing to show. Try one of:');
|
||||
console.error(' openspec show <item>');
|
||||
console.error(' openspec change show');
|
||||
console.error(' openspec spec show');
|
||||
console.error('Or run in an interactive terminal.');
|
||||
}
|
||||
|
||||
private warnIrrelevantFlags(type: ItemType, options: { [k: string]: any }): boolean {
|
||||
const irrelevant: string[] = [];
|
||||
if (type === 'change') {
|
||||
for (const k of SPEC_FLAG_KEYS) if (k in options) irrelevant.push(k);
|
||||
} else {
|
||||
for (const k of CHANGE_FLAG_KEYS) if (k in options) irrelevant.push(k);
|
||||
}
|
||||
if (irrelevant.length > 0) {
|
||||
console.error(`Warning: Ignoring flags not applicable to ${type}: ${irrelevant.join(', ')}`);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
import { program } from 'commander';
|
||||
import { existsSync, readdirSync, readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { MarkdownParser } from '../core/parsers/markdown-parser.js';
|
||||
import { Validator } from '../core/validation/validator.js';
|
||||
import type { Spec } from '../core/schemas/index.js';
|
||||
import { select } from '@inquirer/prompts';
|
||||
import { isInteractive } from '../utils/interactive.js';
|
||||
import { getSpecIds } from '../utils/item-discovery.js';
|
||||
|
||||
const SPECS_DIR = 'openspec/specs';
|
||||
|
||||
export class SpecCommand {
|
||||
private SPECS_DIR = 'openspec/specs';
|
||||
|
||||
async show(specId?: string, options: { json?: boolean; requirements?: boolean; scenarios?: boolean; requirement?: string; noInteractive?: boolean } = {}): Promise<void> {
|
||||
if (!specId) {
|
||||
const canPrompt = isInteractive(options?.noInteractive);
|
||||
const specIds = await getSpecIds();
|
||||
if (canPrompt && specIds.length > 0) {
|
||||
specId = await select({
|
||||
message: 'Select a spec to show',
|
||||
choices: specIds.map(id => ({ name: id, value: id })),
|
||||
});
|
||||
} else {
|
||||
throw new Error('Missing required argument <spec-id>');
|
||||
}
|
||||
}
|
||||
|
||||
const specPath = join(this.SPECS_DIR, specId, 'spec.md');
|
||||
if (!existsSync(specPath)) {
|
||||
throw new Error(`Spec '${specId}' not found at openspec/specs/${specId}/spec.md`);
|
||||
}
|
||||
|
||||
if (options.json) {
|
||||
if (options.requirements && options.requirement) {
|
||||
throw new Error('Options --requirements and --requirement cannot be used together');
|
||||
}
|
||||
const parsed = parseSpecFromFile(specPath, specId);
|
||||
const filtered = filterSpec(parsed, options);
|
||||
const output = {
|
||||
id: specId,
|
||||
title: parsed.name,
|
||||
overview: parsed.overview,
|
||||
requirementCount: filtered.requirements.length,
|
||||
requirements: filtered.requirements,
|
||||
metadata: parsed.metadata ?? { version: '1.0.0', format: 'openspec' as const },
|
||||
};
|
||||
console.log(JSON.stringify(output, null, 2));
|
||||
return;
|
||||
}
|
||||
printSpecTextRaw(specPath);
|
||||
}
|
||||
}
|
||||
|
||||
export function registerSpecCommand(rootProgram: typeof program) {
|
||||
const specCommand = rootProgram
|
||||
.command('spec')
|
||||
.description('Manage and view OpenSpec specifications');
|
||||
|
||||
interface ShowOptions {
|
||||
json?: boolean;
|
||||
// JSON-only filters (raw-first text has no filters)
|
||||
requirements?: boolean;
|
||||
scenarios?: boolean; // --no-scenarios sets this to false (JSON only)
|
||||
requirement?: string; // JSON only
|
||||
}
|
||||
|
||||
function parseSpecFromFile(specPath: string, specId: string): Spec {
|
||||
const content = readFileSync(specPath, 'utf-8');
|
||||
const parser = new MarkdownParser(content);
|
||||
return parser.parseSpec(specId);
|
||||
}
|
||||
|
||||
function validateRequirementIndex(spec: Spec, requirementOpt?: string): number | undefined {
|
||||
if (!requirementOpt) return undefined;
|
||||
const index = Number.parseInt(requirementOpt, 10);
|
||||
if (!Number.isInteger(index) || index < 1 || index > spec.requirements.length) {
|
||||
throw new Error(`Requirement ${requirementOpt} not found`);
|
||||
}
|
||||
return index - 1; // convert to 0-based
|
||||
}
|
||||
|
||||
function filterSpec(spec: Spec, options: ShowOptions): Spec {
|
||||
const requirementIndex = validateRequirementIndex(spec, options.requirement);
|
||||
const includeScenarios = options.scenarios !== false && !options.requirements;
|
||||
|
||||
const filteredRequirements = (requirementIndex !== undefined
|
||||
? [spec.requirements[requirementIndex]]
|
||||
: spec.requirements
|
||||
).map(req => ({
|
||||
text: req.text,
|
||||
scenarios: includeScenarios ? req.scenarios : [],
|
||||
}));
|
||||
|
||||
const metadata = spec.metadata ?? { version: '1.0.0', format: 'openspec' as const };
|
||||
|
||||
return {
|
||||
name: spec.name,
|
||||
overview: spec.overview,
|
||||
requirements: filteredRequirements,
|
||||
metadata,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Print the raw markdown content for a spec file without any formatting.
|
||||
* Raw-first behavior ensures text mode is a passthrough for deterministic output.
|
||||
*/
|
||||
function printSpecTextRaw(specPath: string): void {
|
||||
const content = readFileSync(specPath, 'utf-8');
|
||||
console.log(content);
|
||||
}
|
||||
|
||||
specCommand
|
||||
.command('show [spec-id]')
|
||||
.description('Display a specific specification')
|
||||
.option('--json', 'Output as JSON')
|
||||
.option('--requirements', 'JSON only: Show only requirements (exclude scenarios)')
|
||||
.option('--no-scenarios', 'JSON only: Exclude scenario content')
|
||||
.option('-r, --requirement <id>', 'JSON only: Show specific requirement by ID (1-based)')
|
||||
.option('--no-interactive', 'Disable interactive prompts')
|
||||
.action(async (specId: string | undefined, options: ShowOptions & { noInteractive?: boolean }) => {
|
||||
try {
|
||||
const cmd = new SpecCommand();
|
||||
await cmd.show(specId, options as any);
|
||||
} catch (error) {
|
||||
console.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
});
|
||||
|
||||
specCommand
|
||||
.command('list')
|
||||
.description('List all available specifications')
|
||||
.option('--json', 'Output as JSON')
|
||||
.option('--long', 'Show id and title with counts')
|
||||
.action((options: { json?: boolean; long?: boolean }) => {
|
||||
try {
|
||||
if (!existsSync(SPECS_DIR)) {
|
||||
console.log('No items found');
|
||||
return;
|
||||
}
|
||||
|
||||
const specs = readdirSync(SPECS_DIR, { withFileTypes: true })
|
||||
.filter(dirent => dirent.isDirectory())
|
||||
.map(dirent => {
|
||||
const specPath = join(SPECS_DIR, dirent.name, 'spec.md');
|
||||
if (existsSync(specPath)) {
|
||||
try {
|
||||
const spec = parseSpecFromFile(specPath, dirent.name);
|
||||
|
||||
return {
|
||||
id: dirent.name,
|
||||
title: spec.name,
|
||||
requirementCount: spec.requirements.length
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
id: dirent.name,
|
||||
title: dirent.name,
|
||||
requirementCount: 0
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter((spec): spec is { id: string; title: string; requirementCount: number } => spec !== null)
|
||||
.sort((a, b) => a.id.localeCompare(b.id));
|
||||
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(specs, null, 2));
|
||||
} else {
|
||||
if (specs.length === 0) {
|
||||
console.log('No items found');
|
||||
return;
|
||||
}
|
||||
if (!options.long) {
|
||||
specs.forEach(spec => console.log(spec.id));
|
||||
return;
|
||||
}
|
||||
specs.forEach(spec => {
|
||||
console.log(`${spec.id}: ${spec.title} [requirements ${spec.requirementCount}]`);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
});
|
||||
|
||||
specCommand
|
||||
.command('validate [spec-id]')
|
||||
.description('Validate a specification structure')
|
||||
.option('--strict', 'Enable strict validation mode')
|
||||
.option('--json', 'Output validation report as JSON')
|
||||
.option('--no-interactive', 'Disable interactive prompts')
|
||||
.action(async (specId: string | undefined, options: { strict?: boolean; json?: boolean; noInteractive?: boolean }) => {
|
||||
try {
|
||||
if (!specId) {
|
||||
const canPrompt = isInteractive(options?.noInteractive);
|
||||
const specIds = await getSpecIds();
|
||||
if (canPrompt && specIds.length > 0) {
|
||||
specId = await select({
|
||||
message: 'Select a spec to validate',
|
||||
choices: specIds.map(id => ({ name: id, value: id })),
|
||||
});
|
||||
} else {
|
||||
throw new Error('Missing required argument <spec-id>');
|
||||
}
|
||||
}
|
||||
|
||||
const specPath = join(SPECS_DIR, specId, 'spec.md');
|
||||
|
||||
if (!existsSync(specPath)) {
|
||||
throw new Error(`Spec '${specId}' not found at openspec/specs/${specId}/spec.md`);
|
||||
}
|
||||
|
||||
const validator = new Validator(options.strict);
|
||||
const report = await validator.validateSpec(specPath);
|
||||
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(report, null, 2));
|
||||
} else {
|
||||
if (report.valid) {
|
||||
console.log(`Specification '${specId}' is valid`);
|
||||
} else {
|
||||
console.error(`Specification '${specId}' has issues`);
|
||||
report.issues.forEach(issue => {
|
||||
const label = issue.level === 'ERROR' ? 'ERROR' : issue.level;
|
||||
const prefix = issue.level === 'ERROR' ? '✗' : issue.level === 'WARNING' ? '⚠' : 'ℹ';
|
||||
console.error(`${prefix} [${label}] ${issue.path}: ${issue.message}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
process.exitCode = report.valid ? 0 : 1;
|
||||
} catch (error) {
|
||||
console.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
});
|
||||
|
||||
return specCommand;
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
import { select } from '@inquirer/prompts';
|
||||
import ora from 'ora';
|
||||
import path from 'path';
|
||||
import { Validator } from '../core/validation/validator.js';
|
||||
import { isInteractive } from '../utils/interactive.js';
|
||||
import { getActiveChangeIds, getSpecIds } from '../utils/item-discovery.js';
|
||||
import { nearestMatches } from '../utils/match.js';
|
||||
|
||||
type ItemType = 'change' | 'spec';
|
||||
|
||||
interface ExecuteOptions {
|
||||
all?: boolean;
|
||||
changes?: boolean;
|
||||
specs?: boolean;
|
||||
type?: string;
|
||||
strict?: boolean;
|
||||
json?: boolean;
|
||||
noInteractive?: boolean;
|
||||
concurrency?: string;
|
||||
}
|
||||
|
||||
interface BulkItemResult {
|
||||
id: string;
|
||||
type: ItemType;
|
||||
valid: boolean;
|
||||
issues: { level: 'ERROR' | 'WARNING' | 'INFO'; path: string; message: string }[];
|
||||
durationMs: number;
|
||||
}
|
||||
|
||||
export class ValidateCommand {
|
||||
async execute(itemName: string | undefined, options: ExecuteOptions = {}): Promise<void> {
|
||||
const interactive = isInteractive(options.noInteractive);
|
||||
|
||||
// Handle bulk flags first
|
||||
if (options.all || options.changes || options.specs) {
|
||||
await this.runBulkValidation({
|
||||
changes: !!options.all || !!options.changes,
|
||||
specs: !!options.all || !!options.specs,
|
||||
}, { strict: !!options.strict, json: !!options.json, concurrency: options.concurrency });
|
||||
return;
|
||||
}
|
||||
|
||||
// No item and no flags
|
||||
if (!itemName) {
|
||||
if (interactive) {
|
||||
await this.runInteractiveSelector({ strict: !!options.strict, json: !!options.json, concurrency: options.concurrency });
|
||||
return;
|
||||
}
|
||||
this.printNonInteractiveHint();
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
// Direct item validation with type detection or override
|
||||
const typeOverride = this.normalizeType(options.type);
|
||||
await this.validateDirectItem(itemName, { typeOverride, strict: !!options.strict, json: !!options.json });
|
||||
}
|
||||
|
||||
private normalizeType(value?: string): ItemType | undefined {
|
||||
if (!value) return undefined;
|
||||
const v = value.toLowerCase();
|
||||
if (v === 'change' || v === 'spec') return v;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private async runInteractiveSelector(opts: { strict: boolean; json: boolean; concurrency?: string }): Promise<void> {
|
||||
const choice = await select({
|
||||
message: 'What would you like to validate?',
|
||||
choices: [
|
||||
{ name: 'All (changes + specs)', value: 'all' },
|
||||
{ name: 'All changes', value: 'changes' },
|
||||
{ name: 'All specs', value: 'specs' },
|
||||
{ name: 'Pick a specific change or spec', value: 'one' },
|
||||
],
|
||||
});
|
||||
|
||||
if (choice === 'all') return this.runBulkValidation({ changes: true, specs: true }, opts);
|
||||
if (choice === 'changes') return this.runBulkValidation({ changes: true, specs: false }, opts);
|
||||
if (choice === 'specs') return this.runBulkValidation({ changes: false, specs: true }, opts);
|
||||
|
||||
// one
|
||||
const [changes, specs] = await Promise.all([getActiveChangeIds(), getSpecIds()]);
|
||||
const items: { name: string; value: { type: ItemType; id: string } }[] = [];
|
||||
items.push(...changes.map(id => ({ name: `change/${id}`, value: { type: 'change' as const, id } })));
|
||||
items.push(...specs.map(id => ({ name: `spec/${id}`, value: { type: 'spec' as const, id } })));
|
||||
if (items.length === 0) {
|
||||
console.error('No items found to validate.');
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
const picked = await select<{ type: ItemType; id: string }>({ message: 'Pick an item', choices: items });
|
||||
await this.validateByType(picked.type, picked.id, opts);
|
||||
}
|
||||
|
||||
private printNonInteractiveHint(): void {
|
||||
console.error('Nothing to validate. Try one of:');
|
||||
console.error(' openspec validate --all');
|
||||
console.error(' openspec validate --changes');
|
||||
console.error(' openspec validate --specs');
|
||||
console.error(' openspec validate <item-name>');
|
||||
console.error('Or run in an interactive terminal.');
|
||||
}
|
||||
|
||||
private async validateDirectItem(itemName: string, opts: { typeOverride?: ItemType; strict: boolean; json: boolean }): Promise<void> {
|
||||
const [changes, specs] = await Promise.all([getActiveChangeIds(), getSpecIds()]);
|
||||
const isChange = changes.includes(itemName);
|
||||
const isSpec = specs.includes(itemName);
|
||||
|
||||
const type = opts.typeOverride ?? (isChange ? 'change' : isSpec ? 'spec' : undefined);
|
||||
|
||||
if (!type) {
|
||||
console.error(`Unknown item '${itemName}'`);
|
||||
const suggestions = nearestMatches(itemName, [...changes, ...specs]);
|
||||
if (suggestions.length) console.error(`Did you mean: ${suggestions.join(', ')}?`);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!opts.typeOverride && isChange && isSpec) {
|
||||
console.error(`Ambiguous item '${itemName}' matches both a change and a spec.`);
|
||||
console.error('Pass --type change|spec, or use: openspec change validate / openspec spec validate');
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
await this.validateByType(type, itemName, opts);
|
||||
}
|
||||
|
||||
private async validateByType(type: ItemType, id: string, opts: { strict: boolean; json: boolean }): Promise<void> {
|
||||
const validator = new Validator(opts.strict);
|
||||
if (type === 'change') {
|
||||
const file = path.join(process.cwd(), 'openspec', 'changes', id, 'proposal.md');
|
||||
const start = Date.now();
|
||||
const report = await validator.validateChange(file);
|
||||
const durationMs = Date.now() - start;
|
||||
this.printReport('change', id, report, durationMs, opts.json);
|
||||
process.exitCode = report.valid ? 0 : 1;
|
||||
return;
|
||||
}
|
||||
const file = path.join(process.cwd(), 'openspec', 'specs', id, 'spec.md');
|
||||
const start = Date.now();
|
||||
const report = await validator.validateSpec(file);
|
||||
const durationMs = Date.now() - start;
|
||||
this.printReport('spec', id, report, durationMs, opts.json);
|
||||
process.exitCode = report.valid ? 0 : 1;
|
||||
}
|
||||
|
||||
private printReport(type: ItemType, id: string, report: { valid: boolean; issues: any[] }, durationMs: number, json: boolean): void {
|
||||
if (json) {
|
||||
const out = { items: [{ id, type, valid: report.valid, issues: report.issues, durationMs }], summary: { totals: { items: 1, passed: report.valid ? 1 : 0, failed: report.valid ? 0 : 1 }, byType: { [type]: { items: 1, passed: report.valid ? 1 : 0, failed: report.valid ? 0 : 1 } } }, version: '1.0' };
|
||||
console.log(JSON.stringify(out, null, 2));
|
||||
return;
|
||||
}
|
||||
if (report.valid) {
|
||||
console.log(`${type === 'change' ? 'Change' : 'Specification'} '${id}' is valid`);
|
||||
} else {
|
||||
console.error(`${type === 'change' ? 'Change' : 'Specification'} '${id}' has issues`);
|
||||
for (const issue of report.issues) {
|
||||
const label = issue.level === 'ERROR' ? 'ERROR' : issue.level;
|
||||
const prefix = issue.level === 'ERROR' ? '✗' : issue.level === 'WARNING' ? '⚠' : 'ℹ';
|
||||
console.error(`${prefix} [${label}] ${issue.path}: ${issue.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async runBulkValidation(scope: { changes: boolean; specs: boolean }, opts: { strict: boolean; json: boolean; concurrency?: string }): Promise<void> {
|
||||
const spinner = !opts.json ? ora('Validating...').start() : undefined;
|
||||
const [changeIds, specIds] = await Promise.all([
|
||||
scope.changes ? getActiveChangeIds() : Promise.resolve<string[]>([]),
|
||||
scope.specs ? getSpecIds() : Promise.resolve<string[]>([]),
|
||||
]);
|
||||
|
||||
const DEFAULT_CONCURRENCY = 6;
|
||||
const maxSuggestions = 5; // used by nearestMatches
|
||||
const concurrency = normalizeConcurrency(opts.concurrency) ?? normalizeConcurrency(process.env.OPENSPEC_CONCURRENCY) ?? DEFAULT_CONCURRENCY;
|
||||
const validator = new Validator(opts.strict);
|
||||
const queue: Array<() => Promise<BulkItemResult>> = [];
|
||||
|
||||
for (const id of changeIds) {
|
||||
queue.push(async () => {
|
||||
const start = Date.now();
|
||||
const file = path.join(process.cwd(), 'openspec', 'changes', id, 'proposal.md');
|
||||
const report = await validator.validateChange(file);
|
||||
const durationMs = Date.now() - start;
|
||||
return { id, type: 'change' as const, valid: report.valid, issues: report.issues, durationMs };
|
||||
});
|
||||
}
|
||||
for (const id of specIds) {
|
||||
queue.push(async () => {
|
||||
const start = Date.now();
|
||||
const file = path.join(process.cwd(), 'openspec', 'specs', id, 'spec.md');
|
||||
const report = await validator.validateSpec(file);
|
||||
const durationMs = Date.now() - start;
|
||||
return { id, type: 'spec' as const, valid: report.valid, issues: report.issues, durationMs };
|
||||
});
|
||||
}
|
||||
|
||||
const results: BulkItemResult[] = [];
|
||||
let index = 0;
|
||||
let running = 0;
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
const next = () => {
|
||||
while (running < concurrency && index < queue.length) {
|
||||
const currentIndex = index++;
|
||||
const task = queue[currentIndex];
|
||||
running++;
|
||||
if (spinner) spinner.text = `Validating (${currentIndex + 1}/${queue.length})...`;
|
||||
task()
|
||||
.then(res => {
|
||||
results.push(res);
|
||||
if (res.valid) passed++; else failed++;
|
||||
})
|
||||
.catch((error: any) => {
|
||||
const message = error?.message || 'Unknown error';
|
||||
const res: BulkItemResult = { id: getPlannedId(currentIndex, changeIds, specIds) ?? 'unknown', type: getPlannedType(currentIndex, changeIds, specIds) ?? 'change', valid: false, issues: [{ level: 'ERROR', path: 'file', message }], durationMs: 0 };
|
||||
results.push(res);
|
||||
failed++;
|
||||
})
|
||||
.finally(() => {
|
||||
running--;
|
||||
if (index >= queue.length && running === 0) resolve();
|
||||
else next();
|
||||
});
|
||||
}
|
||||
};
|
||||
next();
|
||||
});
|
||||
|
||||
spinner?.stop();
|
||||
|
||||
results.sort((a, b) => a.id.localeCompare(b.id));
|
||||
const summary = {
|
||||
totals: { items: results.length, passed, failed },
|
||||
byType: {
|
||||
...(scope.changes ? { change: summarizeType(results, 'change') } : {}),
|
||||
...(scope.specs ? { spec: summarizeType(results, 'spec') } : {}),
|
||||
},
|
||||
} as const;
|
||||
|
||||
if (opts.json) {
|
||||
const out = { items: results, summary, version: '1.0' };
|
||||
console.log(JSON.stringify(out, null, 2));
|
||||
} else {
|
||||
for (const res of results) {
|
||||
if (res.valid) console.log(`✓ ${res.type}/${res.id}`);
|
||||
else console.error(`✗ ${res.type}/${res.id}`);
|
||||
}
|
||||
console.log(`Totals: ${summary.totals.passed} passed, ${summary.totals.failed} failed (${summary.totals.items} items)`);
|
||||
}
|
||||
|
||||
process.exitCode = failed > 0 ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
function summarizeType(results: BulkItemResult[], type: ItemType) {
|
||||
const filtered = results.filter(r => r.type === type);
|
||||
const items = filtered.length;
|
||||
const passed = filtered.filter(r => r.valid).length;
|
||||
const failed = items - passed;
|
||||
return { items, passed, failed };
|
||||
}
|
||||
|
||||
function normalizeConcurrency(value?: string): number | undefined {
|
||||
if (!value) return undefined;
|
||||
const n = parseInt(value, 10);
|
||||
if (Number.isNaN(n) || n <= 0) return undefined;
|
||||
return n;
|
||||
}
|
||||
|
||||
function getPlannedId(index: number, changeIds: string[], specIds: string[]): string | undefined {
|
||||
const totalChanges = changeIds.length;
|
||||
if (index < totalChanges) return changeIds[index];
|
||||
const specIndex = index - totalChanges;
|
||||
return specIds[specIndex];
|
||||
}
|
||||
|
||||
function getPlannedType(index: number, changeIds: string[], specIds: string[]): ItemType | undefined {
|
||||
const totalChanges = changeIds.length;
|
||||
if (index < totalChanges) return 'change';
|
||||
const specIndex = index - totalChanges;
|
||||
if (specIndex >= 0 && specIndex < specIds.length) return 'spec';
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,591 @@
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { select, confirm } from '@inquirer/prompts';
|
||||
import { FileSystemUtils } from '../utils/file-system.js';
|
||||
import { getTaskProgressForChange, formatTaskStatus } from '../utils/task-progress.js';
|
||||
import { Validator } from './validation/validator.js';
|
||||
import chalk from 'chalk';
|
||||
import {
|
||||
extractRequirementsSection,
|
||||
parseDeltaSpec,
|
||||
normalizeRequirementName,
|
||||
type RequirementBlock,
|
||||
} from './parsers/requirement-blocks.js';
|
||||
|
||||
interface SpecUpdate {
|
||||
source: string;
|
||||
target: string;
|
||||
exists: boolean;
|
||||
}
|
||||
|
||||
export class ArchiveCommand {
|
||||
async execute(changeName?: string, options: { yes?: boolean; skipSpecs?: boolean; noValidate?: boolean } = {}): Promise<void> {
|
||||
const targetPath = '.';
|
||||
const changesDir = path.join(targetPath, 'openspec', 'changes');
|
||||
const archiveDir = path.join(changesDir, 'archive');
|
||||
const mainSpecsDir = path.join(targetPath, 'openspec', 'specs');
|
||||
|
||||
// Check if changes directory exists
|
||||
try {
|
||||
await fs.access(changesDir);
|
||||
} catch {
|
||||
throw new Error("No OpenSpec changes directory found. Run 'openspec init' first.");
|
||||
}
|
||||
|
||||
// Get change name interactively if not provided
|
||||
if (!changeName) {
|
||||
const selectedChange = await this.selectChange(changesDir);
|
||||
if (!selectedChange) {
|
||||
console.log('No change selected. Aborting.');
|
||||
return;
|
||||
}
|
||||
changeName = selectedChange;
|
||||
}
|
||||
|
||||
const changeDir = path.join(changesDir, changeName);
|
||||
|
||||
// Verify change exists
|
||||
try {
|
||||
const stat = await fs.stat(changeDir);
|
||||
if (!stat.isDirectory()) {
|
||||
throw new Error(`Change '${changeName}' not found.`);
|
||||
}
|
||||
} catch {
|
||||
throw new Error(`Change '${changeName}' not found.`);
|
||||
}
|
||||
|
||||
// Validate specs and change before archiving
|
||||
if (!options.noValidate) {
|
||||
const validator = new Validator();
|
||||
let hasValidationErrors = false;
|
||||
|
||||
// Validate change.md file
|
||||
const changeFile = path.join(changeDir, 'change.md');
|
||||
try {
|
||||
await fs.access(changeFile);
|
||||
const changeReport = await validator.validateChange(changeFile);
|
||||
|
||||
if (!changeReport.valid) {
|
||||
hasValidationErrors = true;
|
||||
console.log(chalk.red(`\nValidation errors in change.md:`));
|
||||
for (const issue of changeReport.issues) {
|
||||
if (issue.level === 'ERROR') {
|
||||
console.log(chalk.red(` ✗ ${issue.message}`));
|
||||
} else if (issue.level === 'WARNING') {
|
||||
console.log(chalk.yellow(` ⚠ ${issue.message}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Change file doesn't exist, skip validation
|
||||
}
|
||||
|
||||
// Validate spec files
|
||||
const changeSpecsDir = path.join(changeDir, 'specs');
|
||||
try {
|
||||
const entries = await fs.readdir(changeSpecsDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
const specFile = path.join(changeSpecsDir, entry.name, 'spec.md');
|
||||
|
||||
try {
|
||||
await fs.access(specFile);
|
||||
const report = await validator.validateSpec(specFile);
|
||||
|
||||
if (!report.valid) {
|
||||
hasValidationErrors = true;
|
||||
console.log(chalk.red(`\nValidation errors in ${entry.name}/spec.md:`));
|
||||
for (const issue of report.issues) {
|
||||
if (issue.level === 'ERROR') {
|
||||
console.log(chalk.red(` ✗ ${issue.message}`));
|
||||
} else if (issue.level === 'WARNING') {
|
||||
console.log(chalk.yellow(` ⚠ ${issue.message}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Spec file doesn't exist, skip validation
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// No specs directory, skip validation
|
||||
}
|
||||
|
||||
if (hasValidationErrors) {
|
||||
console.log(chalk.red('\nValidation failed. Please fix the errors before archiving.'));
|
||||
console.log(chalk.yellow('To skip validation (not recommended), use --no-validate flag.'));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Log warning when validation is skipped
|
||||
const timestamp = new Date().toISOString();
|
||||
|
||||
if (!options.yes) {
|
||||
const proceed = await confirm({
|
||||
message: chalk.yellow('⚠️ WARNING: Skipping validation may archive invalid specs. Continue? (y/N)'),
|
||||
default: false
|
||||
});
|
||||
if (!proceed) {
|
||||
console.log('Archive cancelled.');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
console.log(chalk.yellow(`\n⚠️ WARNING: Skipping validation may archive invalid specs.`));
|
||||
}
|
||||
|
||||
console.log(chalk.yellow(`[${timestamp}] Validation skipped for change: ${changeName}`));
|
||||
console.log(chalk.yellow(`Affected files: ${changeDir}`));
|
||||
}
|
||||
|
||||
// Show progress and check for incomplete tasks
|
||||
const progress = await getTaskProgressForChange(changesDir, changeName);
|
||||
const status = formatTaskStatus(progress);
|
||||
console.log(`Task status: ${status}`);
|
||||
|
||||
const incompleteTasks = Math.max(progress.total - progress.completed, 0);
|
||||
if (incompleteTasks > 0) {
|
||||
if (!options.yes) {
|
||||
const proceed = await confirm({
|
||||
message: `Warning: ${incompleteTasks} incomplete task(s) found. Continue?`,
|
||||
default: false
|
||||
});
|
||||
if (!proceed) {
|
||||
console.log('Archive cancelled.');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
console.log(`Warning: ${incompleteTasks} incomplete task(s) found. Continuing due to --yes flag.`);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle spec updates unless skipSpecs flag is set
|
||||
if (options.skipSpecs) {
|
||||
console.log('Skipping spec updates (--skip-specs flag provided).');
|
||||
} else {
|
||||
// Find specs to update
|
||||
const specUpdates = await this.findSpecUpdates(changeDir, mainSpecsDir);
|
||||
|
||||
if (specUpdates.length > 0) {
|
||||
console.log('\nSpecs to update:');
|
||||
for (const update of specUpdates) {
|
||||
const status = update.exists ? 'update' : 'create';
|
||||
const capability = path.basename(path.dirname(update.target));
|
||||
console.log(` ${capability}: ${status}`);
|
||||
}
|
||||
|
||||
let shouldUpdateSpecs = true;
|
||||
if (!options.yes) {
|
||||
shouldUpdateSpecs = await confirm({
|
||||
message: 'Proceed with spec updates?',
|
||||
default: true
|
||||
});
|
||||
if (!shouldUpdateSpecs) {
|
||||
console.log('Skipping spec updates. Proceeding with archive.');
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldUpdateSpecs) {
|
||||
// Prepare all updates first (validation pass, no writes)
|
||||
const prepared: Array<{ update: SpecUpdate; rebuilt: string; counts: { added: number; modified: number; removed: number; renamed: number } }> = [];
|
||||
try {
|
||||
for (const update of specUpdates) {
|
||||
const built = await this.buildUpdatedSpec(update, changeName!);
|
||||
prepared.push({ update, rebuilt: built.rebuilt, counts: built.counts });
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.log(String(err.message || err));
|
||||
console.log('Aborted. No files were changed.');
|
||||
return;
|
||||
}
|
||||
|
||||
// All validations passed; write files and display counts
|
||||
let totals = { added: 0, modified: 0, removed: 0, renamed: 0 };
|
||||
for (const p of prepared) {
|
||||
await this.writeUpdatedSpec(p.update, p.rebuilt, p.counts);
|
||||
totals.added += p.counts.added;
|
||||
totals.modified += p.counts.modified;
|
||||
totals.removed += p.counts.removed;
|
||||
totals.renamed += p.counts.renamed;
|
||||
}
|
||||
console.log(
|
||||
`Totals: + ${totals.added}, ~ ${totals.modified}, - ${totals.removed}, → ${totals.renamed}`
|
||||
);
|
||||
console.log('Specs updated successfully.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create archive directory with date prefix
|
||||
const archiveName = `${this.getArchiveDate()}-${changeName}`;
|
||||
const archivePath = path.join(archiveDir, archiveName);
|
||||
|
||||
// Check if archive already exists
|
||||
try {
|
||||
await fs.access(archivePath);
|
||||
throw new Error(`Archive '${archiveName}' already exists.`);
|
||||
} catch (error: any) {
|
||||
if (error.code !== 'ENOENT') {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Create archive directory if needed
|
||||
await fs.mkdir(archiveDir, { recursive: true });
|
||||
|
||||
// Move change to archive
|
||||
await fs.rename(changeDir, archivePath);
|
||||
|
||||
console.log(`Change '${changeName}' archived as '${archiveName}'.`);
|
||||
}
|
||||
|
||||
private async selectChange(changesDir: string): Promise<string | null> {
|
||||
// Get all directories in changes (excluding archive)
|
||||
const entries = await fs.readdir(changesDir, { withFileTypes: true });
|
||||
const changeDirs = entries
|
||||
.filter(entry => entry.isDirectory() && entry.name !== 'archive')
|
||||
.map(entry => entry.name)
|
||||
.sort();
|
||||
|
||||
if (changeDirs.length === 0) {
|
||||
console.log('No active changes found.');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Build choices with progress inline to avoid duplicate lists
|
||||
let choices: Array<{ name: string; value: string }> = changeDirs.map(name => ({ name, value: name }));
|
||||
try {
|
||||
const progressList: Array<{ id: string; status: string }> = [];
|
||||
for (const id of changeDirs) {
|
||||
const progress = await getTaskProgressForChange(changesDir, id);
|
||||
const status = formatTaskStatus(progress);
|
||||
progressList.push({ id, status });
|
||||
}
|
||||
const nameWidth = Math.max(...progressList.map(p => p.id.length));
|
||||
choices = progressList.map(p => ({
|
||||
name: `${p.id.padEnd(nameWidth)} ${p.status}`,
|
||||
value: p.id
|
||||
}));
|
||||
} catch {
|
||||
// If anything fails, fall back to simple names
|
||||
choices = changeDirs.map(name => ({ name, value: name }));
|
||||
}
|
||||
|
||||
try {
|
||||
const answer = await select({
|
||||
message: 'Select a change to archive',
|
||||
choices
|
||||
});
|
||||
return answer;
|
||||
} catch (error) {
|
||||
// User cancelled (Ctrl+C)
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Deprecated: replaced by shared task-progress utilities
|
||||
private async checkIncompleteTasks(_tasksPath: string): Promise<number> {
|
||||
return 0;
|
||||
}
|
||||
|
||||
private async findSpecUpdates(changeDir: string, mainSpecsDir: string): Promise<SpecUpdate[]> {
|
||||
const updates: SpecUpdate[] = [];
|
||||
const changeSpecsDir = path.join(changeDir, 'specs');
|
||||
|
||||
try {
|
||||
const entries = await fs.readdir(changeSpecsDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
const specFile = path.join(changeSpecsDir, entry.name, 'spec.md');
|
||||
const targetFile = path.join(mainSpecsDir, entry.name, 'spec.md');
|
||||
|
||||
try {
|
||||
await fs.access(specFile);
|
||||
|
||||
// Check if target exists
|
||||
let exists = false;
|
||||
try {
|
||||
await fs.access(targetFile);
|
||||
exists = true;
|
||||
} catch {
|
||||
exists = false;
|
||||
}
|
||||
|
||||
updates.push({
|
||||
source: specFile,
|
||||
target: targetFile,
|
||||
exists
|
||||
});
|
||||
} catch {
|
||||
// Source spec doesn't exist, skip
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// No specs directory in change
|
||||
}
|
||||
|
||||
return updates;
|
||||
}
|
||||
|
||||
private async buildUpdatedSpec(update: SpecUpdate, changeName: string): Promise<{ rebuilt: string; counts: { added: number; modified: number; removed: number; renamed: number } }> {
|
||||
// Read change spec content (delta-format expected)
|
||||
const changeContent = await fs.readFile(update.source, 'utf-8');
|
||||
|
||||
// Parse deltas from the change spec file
|
||||
const plan = parseDeltaSpec(changeContent);
|
||||
const specName = path.basename(path.dirname(update.target));
|
||||
|
||||
// Pre-validate duplicates within sections
|
||||
const addedNames = new Set<string>();
|
||||
for (const add of plan.added) {
|
||||
const name = normalizeRequirementName(add.name);
|
||||
if (addedNames.has(name)) {
|
||||
throw new Error(
|
||||
`${specName} validation failed - duplicate requirement in ADDED for header "### Requirement: ${add.name}"`
|
||||
);
|
||||
}
|
||||
addedNames.add(name);
|
||||
}
|
||||
const modifiedNames = new Set<string>();
|
||||
for (const mod of plan.modified) {
|
||||
const name = normalizeRequirementName(mod.name);
|
||||
if (modifiedNames.has(name)) {
|
||||
throw new Error(
|
||||
`${specName} validation failed - duplicate requirement in MODIFIED for header "### Requirement: ${mod.name}"`
|
||||
);
|
||||
}
|
||||
modifiedNames.add(name);
|
||||
}
|
||||
const removedNamesSet = new Set<string>();
|
||||
for (const rem of plan.removed) {
|
||||
const name = normalizeRequirementName(rem);
|
||||
if (removedNamesSet.has(name)) {
|
||||
throw new Error(
|
||||
`${specName} validation failed - duplicate requirement in REMOVED for header "### Requirement: ${rem}"`
|
||||
);
|
||||
}
|
||||
removedNamesSet.add(name);
|
||||
}
|
||||
const renamedFromSet = new Set<string>();
|
||||
const renamedToSet = new Set<string>();
|
||||
for (const { from, to } of plan.renamed) {
|
||||
const fromNorm = normalizeRequirementName(from);
|
||||
const toNorm = normalizeRequirementName(to);
|
||||
if (renamedFromSet.has(fromNorm)) {
|
||||
throw new Error(
|
||||
`${specName} validation failed - duplicate FROM in RENAMED for header "### Requirement: ${from}"`
|
||||
);
|
||||
}
|
||||
if (renamedToSet.has(toNorm)) {
|
||||
throw new Error(
|
||||
`${specName} validation failed - duplicate TO in RENAMED for header "### Requirement: ${to}"`
|
||||
);
|
||||
}
|
||||
renamedFromSet.add(fromNorm);
|
||||
renamedToSet.add(toNorm);
|
||||
}
|
||||
|
||||
// Pre-validate cross-section conflicts
|
||||
const conflicts: Array<{ name: string; a: string; b: string }> = [];
|
||||
for (const n of modifiedNames) {
|
||||
if (removedNamesSet.has(n)) conflicts.push({ name: n, a: 'MODIFIED', b: 'REMOVED' });
|
||||
if (addedNames.has(n)) conflicts.push({ name: n, a: 'MODIFIED', b: 'ADDED' });
|
||||
}
|
||||
for (const n of addedNames) {
|
||||
if (removedNamesSet.has(n)) conflicts.push({ name: n, a: 'ADDED', b: 'REMOVED' });
|
||||
}
|
||||
// Renamed interplay: MODIFIED must reference the NEW header, not FROM
|
||||
for (const { from, to } of plan.renamed) {
|
||||
const fromNorm = normalizeRequirementName(from);
|
||||
const toNorm = normalizeRequirementName(to);
|
||||
if (modifiedNames.has(fromNorm)) {
|
||||
throw new Error(
|
||||
`${specName} validation failed - when a rename exists, MODIFIED must reference the NEW header "### Requirement: ${to}"`
|
||||
);
|
||||
}
|
||||
// Detect ADDED colliding with a RENAMED TO
|
||||
if (addedNames.has(toNorm)) {
|
||||
throw new Error(
|
||||
`${specName} validation failed - RENAMED TO header collides with ADDED for "### Requirement: ${to}"`
|
||||
);
|
||||
}
|
||||
}
|
||||
if (conflicts.length > 0) {
|
||||
const c = conflicts[0];
|
||||
throw new Error(
|
||||
`${specName} validation failed - requirement present in multiple sections (${c.a} and ${c.b}) for header "### Requirement: ${c.name}"`
|
||||
);
|
||||
}
|
||||
const hasAnyDelta = (plan.added.length + plan.modified.length + plan.removed.length + plan.renamed.length) > 0;
|
||||
if (!hasAnyDelta) {
|
||||
throw new Error(
|
||||
`Delta parsing found no operations for ${path.basename(path.dirname(update.source))}. ` +
|
||||
`Provide ADDED/MODIFIED/REMOVED/RENAMED sections in change spec.`
|
||||
);
|
||||
}
|
||||
|
||||
// Load or create base target content
|
||||
let targetContent: string;
|
||||
try {
|
||||
targetContent = await fs.readFile(update.target, 'utf-8');
|
||||
} catch {
|
||||
// Target spec does not exist; only ADDED operations are permitted
|
||||
if (plan.modified.length > 0 || plan.removed.length > 0 || plan.renamed.length > 0) {
|
||||
throw new Error(
|
||||
`${specName}: target spec does not exist; only ADDED requirements are allowed for new specs.`
|
||||
);
|
||||
}
|
||||
targetContent = this.buildSpecSkeleton(specName, changeName);
|
||||
}
|
||||
|
||||
// Extract requirements section and build name->block map
|
||||
const parts = extractRequirementsSection(targetContent);
|
||||
const nameToBlock = new Map<string, RequirementBlock>();
|
||||
for (const block of parts.bodyBlocks) {
|
||||
nameToBlock.set(normalizeRequirementName(block.name), block);
|
||||
}
|
||||
|
||||
// Apply operations in order: RENAMED → REMOVED → MODIFIED → ADDED
|
||||
// RENAMED
|
||||
for (const r of plan.renamed) {
|
||||
const from = normalizeRequirementName(r.from);
|
||||
const to = normalizeRequirementName(r.to);
|
||||
if (!nameToBlock.has(from)) {
|
||||
throw new Error(
|
||||
`${specName} RENAMED failed for header "### Requirement: ${r.from}" - source not found`
|
||||
);
|
||||
}
|
||||
if (nameToBlock.has(to)) {
|
||||
throw new Error(
|
||||
`${specName} RENAMED failed for header "### Requirement: ${r.to}" - target already exists`
|
||||
);
|
||||
}
|
||||
const block = nameToBlock.get(from)!;
|
||||
const newHeader = `### Requirement: ${to}`;
|
||||
const rawLines = block.raw.split('\n');
|
||||
rawLines[0] = newHeader;
|
||||
const renamedBlock: RequirementBlock = {
|
||||
headerLine: newHeader,
|
||||
name: to,
|
||||
raw: rawLines.join('\n'),
|
||||
};
|
||||
nameToBlock.delete(from);
|
||||
nameToBlock.set(to, renamedBlock);
|
||||
}
|
||||
|
||||
// REMOVED
|
||||
for (const name of plan.removed) {
|
||||
const key = normalizeRequirementName(name);
|
||||
if (!nameToBlock.has(key)) {
|
||||
throw new Error(
|
||||
`${specName} REMOVED failed for header "### Requirement: ${name}" - not found`
|
||||
);
|
||||
}
|
||||
nameToBlock.delete(key);
|
||||
}
|
||||
|
||||
// MODIFIED
|
||||
for (const mod of plan.modified) {
|
||||
const key = normalizeRequirementName(mod.name);
|
||||
if (!nameToBlock.has(key)) {
|
||||
throw new Error(
|
||||
`${specName} MODIFIED failed for header "### Requirement: ${mod.name}" - not found`
|
||||
);
|
||||
}
|
||||
// Replace block with provided raw (ensure header line matches key)
|
||||
const modHeaderMatch = mod.raw.split('\n')[0].match(/^###\s*Requirement:\s*(.+)\s*$/);
|
||||
if (!modHeaderMatch || normalizeRequirementName(modHeaderMatch[1]) !== key) {
|
||||
throw new Error(
|
||||
`${specName} MODIFIED failed for header "### Requirement: ${mod.name}" - header mismatch in content`
|
||||
);
|
||||
}
|
||||
nameToBlock.set(key, mod);
|
||||
}
|
||||
|
||||
// ADDED
|
||||
for (const add of plan.added) {
|
||||
const key = normalizeRequirementName(add.name);
|
||||
if (nameToBlock.has(key)) {
|
||||
throw new Error(
|
||||
`${specName} ADDED failed for header "### Requirement: ${add.name}" - already exists`
|
||||
);
|
||||
}
|
||||
nameToBlock.set(key, add);
|
||||
}
|
||||
|
||||
// Duplicates within resulting map are implicitly prevented by key uniqueness.
|
||||
|
||||
// Recompose requirements section preserving original ordering where possible
|
||||
const keptOrder: RequirementBlock[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const block of parts.bodyBlocks) {
|
||||
const key = normalizeRequirementName(block.name);
|
||||
const replacement = nameToBlock.get(key);
|
||||
if (replacement) {
|
||||
keptOrder.push(replacement);
|
||||
seen.add(key);
|
||||
}
|
||||
}
|
||||
// Append any newly added that were not in original order
|
||||
for (const [key, block] of nameToBlock.entries()) {
|
||||
if (!seen.has(key)) {
|
||||
keptOrder.push(block);
|
||||
}
|
||||
}
|
||||
|
||||
const reqBody = [
|
||||
parts.preamble && parts.preamble.trim() ? parts.preamble.trimEnd() : ''
|
||||
]
|
||||
.filter(Boolean)
|
||||
.concat(keptOrder.map(b => b.raw))
|
||||
.join('\n\n')
|
||||
.trimEnd();
|
||||
|
||||
const rebuilt = [
|
||||
parts.before.trimEnd(),
|
||||
parts.headerLine,
|
||||
reqBody,
|
||||
parts.after
|
||||
]
|
||||
.filter((s, idx) => !(idx === 0 && s === ''))
|
||||
.join('\n')
|
||||
.replace(/\n{3,}/g, '\n\n');
|
||||
|
||||
return {
|
||||
rebuilt,
|
||||
counts: {
|
||||
added: plan.added.length,
|
||||
modified: plan.modified.length,
|
||||
removed: plan.removed.length,
|
||||
renamed: plan.renamed.length,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private async writeUpdatedSpec(update: SpecUpdate, rebuilt: string, counts: { added: number; modified: number; removed: number; renamed: number }): Promise<void> {
|
||||
// Create target directory if needed
|
||||
const targetDir = path.dirname(update.target);
|
||||
await fs.mkdir(targetDir, { recursive: true });
|
||||
await fs.writeFile(update.target, rebuilt);
|
||||
|
||||
const specName = path.basename(path.dirname(update.target));
|
||||
console.log(`Applying changes to openspec/specs/${specName}/spec.md:`);
|
||||
if (counts.added) console.log(` + ${counts.added} added`);
|
||||
if (counts.modified) console.log(` ~ ${counts.modified} modified`);
|
||||
if (counts.removed) console.log(` - ${counts.removed} removed`);
|
||||
if (counts.renamed) console.log(` → ${counts.renamed} renamed`);
|
||||
}
|
||||
|
||||
private buildSpecSkeleton(specFolderName: string, changeName: string): string {
|
||||
const titleBase = specFolderName;
|
||||
return `# ${titleBase} Specification\n\n## Purpose\nTBD - created by archiving change ${changeName}. Update Purpose after archive.\n\n## Requirements\n`;
|
||||
}
|
||||
|
||||
private getArchiveDate(): string {
|
||||
// Returns date in YYYY-MM-DD format
|
||||
return new Date().toISOString().split('T')[0];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
export const OPENSPEC_DIR_NAME = 'openspec';
|
||||
|
||||
export interface OpenSpecConfig {
|
||||
aiTools: string[];
|
||||
}
|
||||
|
||||
export const OPENSPEC_MARKERS = {
|
||||
start: '<!-- OPENSPEC:START -->',
|
||||
end: '<!-- OPENSPEC:END -->'
|
||||
};
|
||||
|
||||
export const AI_TOOLS = [
|
||||
{ name: 'Claude Code', value: 'claude', available: true },
|
||||
{ name: 'Cursor', value: 'cursor', available: false },
|
||||
{ name: 'Aider', value: 'aider', available: false },
|
||||
{ name: 'Continue', value: 'continue', available: false }
|
||||
];
|
||||
@@ -0,0 +1,6 @@
|
||||
export interface ToolConfigurator {
|
||||
name: string;
|
||||
configFileName: string;
|
||||
isAvailable: boolean;
|
||||
configure(projectPath: string, openspecDir: string): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import path from 'path';
|
||||
import { ToolConfigurator } from './base.js';
|
||||
import { FileSystemUtils } from '../../utils/file-system.js';
|
||||
import { TemplateManager } from '../templates/index.js';
|
||||
import { OPENSPEC_MARKERS } from '../config.js';
|
||||
|
||||
export class ClaudeConfigurator implements ToolConfigurator {
|
||||
name = 'Claude Code';
|
||||
configFileName = 'CLAUDE.md';
|
||||
isAvailable = true;
|
||||
|
||||
async configure(projectPath: string, openspecDir: string): Promise<void> {
|
||||
const filePath = path.join(projectPath, this.configFileName);
|
||||
const content = TemplateManager.getClaudeTemplate();
|
||||
|
||||
await FileSystemUtils.updateFileWithMarkers(
|
||||
filePath,
|
||||
content,
|
||||
OPENSPEC_MARKERS.start,
|
||||
OPENSPEC_MARKERS.end
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { ToolConfigurator } from './base.js';
|
||||
import { ClaudeConfigurator } from './claude.js';
|
||||
|
||||
export class ToolRegistry {
|
||||
private static tools: Map<string, ToolConfigurator> = new Map();
|
||||
|
||||
static {
|
||||
const claudeConfigurator = new ClaudeConfigurator();
|
||||
// Register with the ID that matches the checkbox value
|
||||
this.tools.set('claude', claudeConfigurator);
|
||||
}
|
||||
|
||||
static register(tool: ToolConfigurator): void {
|
||||
this.tools.set(tool.name.toLowerCase().replace(/\s+/g, '-'), tool);
|
||||
}
|
||||
|
||||
static get(toolId: string): ToolConfigurator | undefined {
|
||||
return this.tools.get(toolId);
|
||||
}
|
||||
|
||||
static getAll(): ToolConfigurator[] {
|
||||
return Array.from(this.tools.values());
|
||||
}
|
||||
|
||||
static getAvailable(): ToolConfigurator[] {
|
||||
return this.getAll().filter(tool => tool.isAvailable);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { readFileSync } from 'fs';
|
||||
import path from 'path';
|
||||
import { MarkdownParser } from '../parsers/markdown-parser.js';
|
||||
import { ChangeParser } from '../parsers/change-parser.js';
|
||||
import { Spec, Change } from '../schemas/index.js';
|
||||
|
||||
export class JsonConverter {
|
||||
convertSpecToJson(filePath: string): string {
|
||||
const content = readFileSync(filePath, 'utf-8');
|
||||
const parser = new MarkdownParser(content);
|
||||
const specName = this.extractNameFromPath(filePath);
|
||||
|
||||
const spec = parser.parseSpec(specName);
|
||||
|
||||
const jsonSpec = {
|
||||
...spec,
|
||||
metadata: {
|
||||
...spec.metadata,
|
||||
sourcePath: filePath,
|
||||
},
|
||||
};
|
||||
|
||||
return JSON.stringify(jsonSpec, null, 2);
|
||||
}
|
||||
|
||||
async convertChangeToJson(filePath: string): Promise<string> {
|
||||
const content = readFileSync(filePath, 'utf-8');
|
||||
const changeName = this.extractNameFromPath(filePath);
|
||||
const changeDir = path.dirname(filePath);
|
||||
const parser = new ChangeParser(content, changeDir);
|
||||
|
||||
const change = await parser.parseChangeWithDeltas(changeName);
|
||||
|
||||
const jsonChange = {
|
||||
...change,
|
||||
metadata: {
|
||||
...change.metadata,
|
||||
sourcePath: filePath,
|
||||
},
|
||||
};
|
||||
|
||||
return JSON.stringify(jsonChange, null, 2);
|
||||
}
|
||||
|
||||
private extractNameFromPath(filePath: string): string {
|
||||
const parts = filePath.split('/');
|
||||
|
||||
for (let i = parts.length - 1; i >= 0; i--) {
|
||||
if (parts[i] === 'specs' || parts[i] === 'changes') {
|
||||
if (i < parts.length - 1) {
|
||||
return parts[i + 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fileName = parts[parts.length - 1];
|
||||
return fileName.replace('.md', '');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import chalk from 'chalk';
|
||||
import { diffStringsUnified } from 'jest-diff';
|
||||
import { select } from '@inquirer/prompts';
|
||||
import { Validator } from './validation/validator.js';
|
||||
|
||||
// Constants
|
||||
const ARCHIVE_DIR = 'archive';
|
||||
const MARKDOWN_EXT = '.md';
|
||||
const OPENSPEC_DIR = 'openspec';
|
||||
const CHANGES_DIR = 'changes';
|
||||
const SPECS_DIR = 'specs';
|
||||
|
||||
export class DiffCommand {
|
||||
private filesChanged: number = 0;
|
||||
private linesAdded: number = 0;
|
||||
private linesRemoved: number = 0;
|
||||
|
||||
async execute(changeName?: string): Promise<void> {
|
||||
const changesDir = path.join(process.cwd(), OPENSPEC_DIR, CHANGES_DIR);
|
||||
|
||||
try {
|
||||
await fs.access(changesDir);
|
||||
} catch {
|
||||
throw new Error('No OpenSpec changes directory found');
|
||||
}
|
||||
|
||||
if (!changeName) {
|
||||
changeName = await this.selectChange(changesDir);
|
||||
if (!changeName) return;
|
||||
}
|
||||
|
||||
const changeDir = path.join(changesDir, changeName);
|
||||
|
||||
try {
|
||||
await fs.access(changeDir);
|
||||
} catch {
|
||||
throw new Error(`Change '${changeName}' not found`);
|
||||
}
|
||||
|
||||
const changeSpecsDir = path.join(changeDir, SPECS_DIR);
|
||||
|
||||
try {
|
||||
await fs.access(changeSpecsDir);
|
||||
} catch {
|
||||
console.log(`No spec changes found for '${changeName}'`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate specs and show warnings (non-blocking)
|
||||
const validator = new Validator();
|
||||
let hasWarnings = false;
|
||||
|
||||
try {
|
||||
const entries = await fs.readdir(changeSpecsDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
const specFile = path.join(changeSpecsDir, entry.name, 'spec.md');
|
||||
|
||||
try {
|
||||
await fs.access(specFile);
|
||||
const report = await validator.validateSpec(specFile);
|
||||
|
||||
if (report.issues.length > 0) {
|
||||
const warnings = report.issues.filter(i => i.level === 'WARNING');
|
||||
const errors = report.issues.filter(i => i.level === 'ERROR');
|
||||
|
||||
if (errors.length > 0 || warnings.length > 0) {
|
||||
if (!hasWarnings) {
|
||||
console.log(chalk.yellow('\n⚠️ Validation warnings found:'));
|
||||
hasWarnings = true;
|
||||
}
|
||||
|
||||
console.log(chalk.yellow(`\n ${entry.name}/spec.md:`));
|
||||
for (const issue of errors) {
|
||||
console.log(chalk.red(` ✗ ${issue.message}`));
|
||||
}
|
||||
for (const issue of warnings) {
|
||||
console.log(chalk.yellow(` ⚠ ${issue.message}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Spec file doesn't exist, skip validation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasWarnings) {
|
||||
console.log(chalk.yellow('\nConsider fixing these issues before archiving.\n'));
|
||||
}
|
||||
} catch {
|
||||
// No specs directory, skip validation
|
||||
}
|
||||
|
||||
// Reset counters
|
||||
this.filesChanged = 0;
|
||||
this.linesAdded = 0;
|
||||
this.linesRemoved = 0;
|
||||
|
||||
await this.showDiffs(changeSpecsDir);
|
||||
|
||||
// Show summary
|
||||
if (this.filesChanged > 0) {
|
||||
console.log(chalk.bold(`\n📊 Summary: ${this.filesChanged} file(s) changed, ${chalk.green(`+${this.linesAdded}`)} ${chalk.red(`-${this.linesRemoved}`)}`));
|
||||
}
|
||||
}
|
||||
|
||||
private async selectChange(changesDir: string): Promise<string | undefined> {
|
||||
const entries = await fs.readdir(changesDir, { withFileTypes: true });
|
||||
const changes = entries
|
||||
.filter(entry => entry.isDirectory() && entry.name !== ARCHIVE_DIR)
|
||||
.map(entry => entry.name);
|
||||
|
||||
if (changes.length === 0) {
|
||||
console.log('No changes found');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
console.log('Available changes:');
|
||||
const choices = changes.map((name) => ({
|
||||
name: name,
|
||||
value: name
|
||||
}));
|
||||
|
||||
const answer = await select({
|
||||
message: 'Select a change',
|
||||
choices
|
||||
});
|
||||
|
||||
return answer as string;
|
||||
}
|
||||
|
||||
private async showDiffs(changeSpecsDir: string): Promise<void> {
|
||||
const currentSpecsDir = path.join(process.cwd(), OPENSPEC_DIR, SPECS_DIR);
|
||||
await this.walkAndDiff(changeSpecsDir, currentSpecsDir, '');
|
||||
}
|
||||
|
||||
private async walkAndDiff(changeDir: string, currentDir: string, relativePath: string): Promise<void> {
|
||||
const entries = await fs.readdir(path.join(changeDir, relativePath), { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const entryPath = path.join(relativePath, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
await this.walkAndDiff(changeDir, currentDir, entryPath);
|
||||
} else if (entry.isFile() && entry.name.endsWith(MARKDOWN_EXT)) {
|
||||
await this.diffFile(
|
||||
path.join(changeDir, entryPath),
|
||||
path.join(currentDir, entryPath),
|
||||
entryPath
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async diffFile(changePath: string, currentPath: string, displayPath: string): Promise<void> {
|
||||
let changeContent = '';
|
||||
let currentContent = '';
|
||||
let isNewFile = false;
|
||||
let isDeleted = false;
|
||||
|
||||
try {
|
||||
changeContent = await fs.readFile(changePath, 'utf-8');
|
||||
} catch {
|
||||
changeContent = '';
|
||||
}
|
||||
|
||||
try {
|
||||
currentContent = await fs.readFile(currentPath, 'utf-8');
|
||||
} catch {
|
||||
currentContent = '';
|
||||
isNewFile = true;
|
||||
}
|
||||
|
||||
if (changeContent === currentContent) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (changeContent === '' && currentContent !== '') {
|
||||
isDeleted = true;
|
||||
}
|
||||
|
||||
// Enhanced header with file status
|
||||
console.log(chalk.bold.cyan(`\n${'═'.repeat(60)}`));
|
||||
console.log(chalk.bold.cyan(`📄 ${displayPath}`));
|
||||
|
||||
if (isNewFile) {
|
||||
console.log(chalk.green(` Status: NEW FILE`));
|
||||
} else if (isDeleted) {
|
||||
console.log(chalk.red(` Status: DELETED`));
|
||||
} else {
|
||||
console.log(chalk.yellow(` Status: MODIFIED`));
|
||||
}
|
||||
|
||||
// Use jest-diff for the actual diff with custom options
|
||||
const diffOptions = {
|
||||
aAnnotation: 'Current',
|
||||
bAnnotation: 'Proposed',
|
||||
aColor: chalk.red,
|
||||
bColor: chalk.green,
|
||||
commonColor: chalk.gray,
|
||||
contextLines: 3,
|
||||
expand: false,
|
||||
includeChangeCounts: true,
|
||||
};
|
||||
|
||||
const diff = diffStringsUnified(currentContent, changeContent, diffOptions);
|
||||
|
||||
// Count lines for statistics (approximate)
|
||||
const addedLines = (diff.match(/^\+[^+]/gm) || []).length;
|
||||
const removedLines = (diff.match(/^-[^-]/gm) || []).length;
|
||||
|
||||
console.log(chalk.gray(` Lines: ${chalk.green(`+${addedLines}`)} ${chalk.red(`-${removedLines}`)}`));
|
||||
console.log(chalk.bold.cyan(`${'─'.repeat(60)}\n`));
|
||||
|
||||
// Display the diff
|
||||
console.log(diff);
|
||||
|
||||
// Update counters
|
||||
this.filesChanged++;
|
||||
this.linesAdded += addedLines;
|
||||
this.linesRemoved += removedLines;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import path from 'path';
|
||||
import { select } from '@inquirer/prompts';
|
||||
import ora from 'ora';
|
||||
import { FileSystemUtils } from '../utils/file-system.js';
|
||||
import { TemplateManager, ProjectContext } from './templates/index.js';
|
||||
import { ToolRegistry } from './configurators/registry.js';
|
||||
import { OpenSpecConfig, AI_TOOLS, OPENSPEC_DIR_NAME } from './config.js';
|
||||
|
||||
export class InitCommand {
|
||||
async execute(targetPath: string): Promise<void> {
|
||||
const projectPath = path.resolve(targetPath);
|
||||
const openspecDir = OPENSPEC_DIR_NAME;
|
||||
const openspecPath = path.join(projectPath, openspecDir);
|
||||
|
||||
// Validation happens silently in the background
|
||||
await this.validate(projectPath, openspecPath);
|
||||
|
||||
// Get configuration (after validation to avoid prompts if validation fails)
|
||||
const config = await this.getConfiguration();
|
||||
|
||||
// Step 1: Create directory structure
|
||||
const structureSpinner = ora({ text: 'Creating OpenSpec structure...', stream: process.stdout }).start();
|
||||
await this.createDirectoryStructure(openspecPath);
|
||||
await this.generateFiles(openspecPath, config);
|
||||
structureSpinner.succeed('OpenSpec structure created');
|
||||
|
||||
// Step 2: Configure AI tools
|
||||
const toolSpinner = ora({ text: 'Configuring AI tools...', stream: process.stdout }).start();
|
||||
await this.configureAITools(projectPath, openspecDir, config.aiTools);
|
||||
toolSpinner.succeed('AI tools configured');
|
||||
|
||||
// Success message
|
||||
this.displaySuccessMessage(openspecDir, config);
|
||||
}
|
||||
|
||||
private async validate(projectPath: string, openspecPath: string): Promise<void> {
|
||||
// Check if OpenSpec already exists
|
||||
if (await FileSystemUtils.directoryExists(openspecPath)) {
|
||||
throw new Error(
|
||||
`OpenSpec seems to already be initialized at ${openspecPath}.\n` +
|
||||
`Use 'openspec update' to update the structure.`
|
||||
);
|
||||
}
|
||||
|
||||
// Check write permissions
|
||||
if (!await FileSystemUtils.ensureWritePermissions(projectPath)) {
|
||||
throw new Error(`Insufficient permissions to write to ${projectPath}`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private async getConfiguration(): Promise<OpenSpecConfig> {
|
||||
const config: OpenSpecConfig = {
|
||||
aiTools: []
|
||||
};
|
||||
|
||||
// Single-select for better UX
|
||||
const selectedTool = await select({
|
||||
message: 'Which AI tool do you use?',
|
||||
choices: AI_TOOLS.map(tool => ({
|
||||
name: tool.available ? tool.name : `${tool.name} (coming soon)`,
|
||||
value: tool.value,
|
||||
disabled: !tool.available
|
||||
}))
|
||||
});
|
||||
|
||||
config.aiTools = [selectedTool as string];
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
private async createDirectoryStructure(openspecPath: string): Promise<void> {
|
||||
const directories = [
|
||||
openspecPath,
|
||||
path.join(openspecPath, 'specs'),
|
||||
path.join(openspecPath, 'changes'),
|
||||
path.join(openspecPath, 'changes', 'archive')
|
||||
];
|
||||
|
||||
for (const dir of directories) {
|
||||
await FileSystemUtils.createDirectory(dir);
|
||||
}
|
||||
}
|
||||
|
||||
private async generateFiles(openspecPath: string, config: OpenSpecConfig): Promise<void> {
|
||||
const context: ProjectContext = {
|
||||
// Could be enhanced with prompts for project details
|
||||
};
|
||||
|
||||
const templates = TemplateManager.getTemplates(context);
|
||||
|
||||
for (const template of templates) {
|
||||
const filePath = path.join(openspecPath, template.path);
|
||||
const content = typeof template.content === 'function'
|
||||
? template.content(context)
|
||||
: template.content;
|
||||
|
||||
await FileSystemUtils.writeFile(filePath, content);
|
||||
}
|
||||
}
|
||||
|
||||
private async configureAITools(projectPath: string, openspecDir: string, toolIds: string[]): Promise<void> {
|
||||
for (const toolId of toolIds) {
|
||||
const configurator = ToolRegistry.get(toolId);
|
||||
if (configurator && configurator.isAvailable) {
|
||||
await configurator.configure(projectPath, openspecDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private displaySuccessMessage(openspecDir: string, config: OpenSpecConfig): void {
|
||||
console.log(); // Empty line for spacing
|
||||
ora().succeed('OpenSpec initialized successfully!');
|
||||
|
||||
// Get the selected tool name for display
|
||||
const selectedToolId = config.aiTools[0];
|
||||
const selectedTool = AI_TOOLS.find(t => t.value === selectedToolId);
|
||||
const toolName = selectedTool ? selectedTool.name : 'your AI assistant';
|
||||
|
||||
console.log(`\nNext steps - Copy these prompts to ${toolName}:\n`);
|
||||
console.log('────────────────────────────────────────────────────────────');
|
||||
console.log('1. Populate your project context:');
|
||||
console.log(' "Please read openspec/project.md and help me fill it out');
|
||||
console.log(' with details about my project, tech stack, and conventions"\n');
|
||||
console.log('2. Create your first change proposal:');
|
||||
console.log(' "I want to add [YOUR FEATURE HERE]. Please create an');
|
||||
console.log(' OpenSpec change proposal for this feature"\n');
|
||||
console.log('3. Learn the OpenSpec workflow:');
|
||||
console.log(' "Please explain the OpenSpec workflow from openspec/README.md');
|
||||
console.log(' and how I should work with you on this project"');
|
||||
console.log('────────────────────────────────────────────────────────────\n');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { getTaskProgressForChange, formatTaskStatus } from '../utils/task-progress.js';
|
||||
|
||||
interface ChangeInfo {
|
||||
name: string;
|
||||
completedTasks: number;
|
||||
totalTasks: number;
|
||||
}
|
||||
|
||||
export class ListCommand {
|
||||
async execute(targetPath: string = '.'): Promise<void> {
|
||||
const changesDir = path.join(targetPath, 'openspec', 'changes');
|
||||
|
||||
// Check if changes directory exists
|
||||
try {
|
||||
await fs.access(changesDir);
|
||||
} catch {
|
||||
throw new Error("No OpenSpec changes directory found. Run 'openspec init' first.");
|
||||
}
|
||||
|
||||
// Get all directories in changes (excluding archive)
|
||||
const entries = await fs.readdir(changesDir, { withFileTypes: true });
|
||||
const changeDirs = entries
|
||||
.filter(entry => entry.isDirectory() && entry.name !== 'archive')
|
||||
.map(entry => entry.name);
|
||||
|
||||
if (changeDirs.length === 0) {
|
||||
console.log('No active changes found.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Collect information about each change
|
||||
const changes: ChangeInfo[] = [];
|
||||
|
||||
for (const changeDir of changeDirs) {
|
||||
const progress = await getTaskProgressForChange(changesDir, changeDir);
|
||||
changes.push({
|
||||
name: changeDir,
|
||||
completedTasks: progress.completed,
|
||||
totalTasks: progress.total
|
||||
});
|
||||
}
|
||||
|
||||
// Sort alphabetically by name
|
||||
changes.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
// Display results
|
||||
console.log('Changes:');
|
||||
for (const change of changes) {
|
||||
const padding = ' ';
|
||||
const nameWidth = Math.max(...changes.map(c => c.name.length));
|
||||
const paddedName = change.name.padEnd(nameWidth);
|
||||
|
||||
const status = formatTaskStatus({ total: change.totalTasks, completed: change.completedTasks });
|
||||
|
||||
console.log(`${padding}${paddedName} ${status}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
import { MarkdownParser, Section } from './markdown-parser.js';
|
||||
import { Change, Delta, DeltaOperation, Requirement } from '../schemas/index.js';
|
||||
import path from 'path';
|
||||
import { promises as fs } from 'fs';
|
||||
|
||||
interface DeltaSection {
|
||||
operation: DeltaOperation;
|
||||
requirements: Requirement[];
|
||||
renames?: Array<{ from: string; to: string }>;
|
||||
}
|
||||
|
||||
export class ChangeParser extends MarkdownParser {
|
||||
private changeDir: string;
|
||||
|
||||
constructor(content: string, changeDir: string) {
|
||||
super(content);
|
||||
this.changeDir = changeDir;
|
||||
}
|
||||
|
||||
async parseChangeWithDeltas(name: string): Promise<Change> {
|
||||
const sections = this.parseSections();
|
||||
const why = this.findSection(sections, 'Why')?.content || '';
|
||||
const whatChanges = this.findSection(sections, 'What Changes')?.content || '';
|
||||
|
||||
if (!why) {
|
||||
throw new Error('Change must have a Why section');
|
||||
}
|
||||
|
||||
if (!whatChanges) {
|
||||
throw new Error('Change must have a What Changes section');
|
||||
}
|
||||
|
||||
// Parse deltas from the What Changes section (simple format)
|
||||
const simpleDeltas = this.parseDeltas(whatChanges);
|
||||
|
||||
// Check if there are spec files with delta format
|
||||
const specsDir = path.join(this.changeDir, 'specs');
|
||||
const deltaDeltas = await this.parseDeltaSpecs(specsDir);
|
||||
|
||||
// Combine both types of deltas, preferring delta format if available
|
||||
const deltas = deltaDeltas.length > 0 ? deltaDeltas : simpleDeltas;
|
||||
|
||||
return {
|
||||
name,
|
||||
why: why.trim(),
|
||||
whatChanges: whatChanges.trim(),
|
||||
deltas,
|
||||
metadata: {
|
||||
version: '1.0.0',
|
||||
format: 'openspec-change',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async parseDeltaSpecs(specsDir: string): Promise<Delta[]> {
|
||||
const deltas: Delta[] = [];
|
||||
|
||||
try {
|
||||
const specDirs = await fs.readdir(specsDir, { withFileTypes: true });
|
||||
|
||||
for (const dir of specDirs) {
|
||||
if (!dir.isDirectory()) continue;
|
||||
|
||||
const specName = dir.name;
|
||||
const specFile = path.join(specsDir, specName, 'spec.md');
|
||||
|
||||
try {
|
||||
const content = await fs.readFile(specFile, 'utf-8');
|
||||
const specDeltas = this.parseSpecDeltas(specName, content);
|
||||
deltas.push(...specDeltas);
|
||||
} catch (error) {
|
||||
// Spec file might not exist, which is okay
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Specs directory might not exist, which is okay
|
||||
return [];
|
||||
}
|
||||
|
||||
return deltas;
|
||||
}
|
||||
|
||||
private parseSpecDeltas(specName: string, content: string): Delta[] {
|
||||
const deltas: Delta[] = [];
|
||||
const sections = this.parseSectionsFromContent(content);
|
||||
|
||||
// Parse ADDED requirements
|
||||
const addedSection = this.findSection(sections, 'ADDED Requirements');
|
||||
if (addedSection) {
|
||||
const requirements = this.parseRequirements(addedSection);
|
||||
requirements.forEach(req => {
|
||||
deltas.push({
|
||||
spec: specName,
|
||||
operation: 'ADDED' as DeltaOperation,
|
||||
description: `Add requirement: ${req.text}`,
|
||||
// Provide both single and plural forms for compatibility
|
||||
requirement: req,
|
||||
requirements: [req],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Parse MODIFIED requirements
|
||||
const modifiedSection = this.findSection(sections, 'MODIFIED Requirements');
|
||||
if (modifiedSection) {
|
||||
const requirements = this.parseRequirements(modifiedSection);
|
||||
requirements.forEach(req => {
|
||||
deltas.push({
|
||||
spec: specName,
|
||||
operation: 'MODIFIED' as DeltaOperation,
|
||||
description: `Modify requirement: ${req.text}`,
|
||||
requirement: req,
|
||||
requirements: [req],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Parse REMOVED requirements
|
||||
const removedSection = this.findSection(sections, 'REMOVED Requirements');
|
||||
if (removedSection) {
|
||||
const requirements = this.parseRequirements(removedSection);
|
||||
requirements.forEach(req => {
|
||||
deltas.push({
|
||||
spec: specName,
|
||||
operation: 'REMOVED' as DeltaOperation,
|
||||
description: `Remove requirement: ${req.text}`,
|
||||
requirement: req,
|
||||
requirements: [req],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Parse RENAMED requirements
|
||||
const renamedSection = this.findSection(sections, 'RENAMED Requirements');
|
||||
if (renamedSection) {
|
||||
const renames = this.parseRenames(renamedSection.content);
|
||||
renames.forEach(rename => {
|
||||
deltas.push({
|
||||
spec: specName,
|
||||
operation: 'RENAMED' as DeltaOperation,
|
||||
description: `Rename requirement from "${rename.from}" to "${rename.to}"`,
|
||||
rename,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return deltas;
|
||||
}
|
||||
|
||||
private parseRenames(content: string): Array<{ from: string; to: string }> {
|
||||
const renames: Array<{ from: string; to: string }> = [];
|
||||
const lines = content.split('\n');
|
||||
|
||||
let currentRename: { from?: string; to?: string } = {};
|
||||
|
||||
for (const line of lines) {
|
||||
const fromMatch = line.match(/^\s*-?\s*FROM:\s*`?###\s*Requirement:\s*(.+?)`?\s*$/);
|
||||
const toMatch = line.match(/^\s*-?\s*TO:\s*`?###\s*Requirement:\s*(.+?)`?\s*$/);
|
||||
|
||||
if (fromMatch) {
|
||||
currentRename.from = fromMatch[1].trim();
|
||||
} else if (toMatch) {
|
||||
currentRename.to = toMatch[1].trim();
|
||||
|
||||
if (currentRename.from && currentRename.to) {
|
||||
renames.push({
|
||||
from: currentRename.from,
|
||||
to: currentRename.to,
|
||||
});
|
||||
currentRename = {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return renames;
|
||||
}
|
||||
|
||||
private parseSectionsFromContent(content: string): Section[] {
|
||||
const lines = content.split('\n');
|
||||
const sections: Section[] = [];
|
||||
const stack: Section[] = [];
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
const headerMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
||||
|
||||
if (headerMatch) {
|
||||
const level = headerMatch[1].length;
|
||||
const title = headerMatch[2].trim();
|
||||
const contentLines = this.getContentUntilNextHeaderFromLines(lines, i + 1, level);
|
||||
|
||||
const section = {
|
||||
level,
|
||||
title,
|
||||
content: contentLines.join('\n').trim(),
|
||||
children: [],
|
||||
};
|
||||
|
||||
while (stack.length > 0 && stack[stack.length - 1].level >= level) {
|
||||
stack.pop();
|
||||
}
|
||||
|
||||
if (stack.length === 0) {
|
||||
sections.push(section);
|
||||
} else {
|
||||
stack[stack.length - 1].children.push(section);
|
||||
}
|
||||
|
||||
stack.push(section);
|
||||
}
|
||||
}
|
||||
|
||||
return sections;
|
||||
}
|
||||
|
||||
private getContentUntilNextHeaderFromLines(lines: string[], startLine: number, currentLevel: number): string[] {
|
||||
const contentLines: string[] = [];
|
||||
|
||||
for (let i = startLine; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
const headerMatch = line.match(/^(#{1,6})\s+/);
|
||||
|
||||
if (headerMatch && headerMatch[1].length <= currentLevel) {
|
||||
break;
|
||||
}
|
||||
|
||||
contentLines.push(line);
|
||||
}
|
||||
|
||||
return contentLines;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
import { Spec, Change, Requirement, Scenario, Delta, DeltaOperation } from '../schemas/index.js';
|
||||
|
||||
export interface Section {
|
||||
level: number;
|
||||
title: string;
|
||||
content: string;
|
||||
children: Section[];
|
||||
}
|
||||
|
||||
export class MarkdownParser {
|
||||
private lines: string[];
|
||||
private currentLine: number;
|
||||
|
||||
constructor(content: string) {
|
||||
this.lines = content.split('\n');
|
||||
this.currentLine = 0;
|
||||
}
|
||||
|
||||
parseSpec(name: string): Spec {
|
||||
const sections = this.parseSections();
|
||||
const purpose = this.findSection(sections, 'Purpose')?.content || '';
|
||||
|
||||
const requirementsSection = this.findSection(sections, 'Requirements');
|
||||
|
||||
if (!purpose) {
|
||||
throw new Error('Spec must have a Purpose section');
|
||||
}
|
||||
|
||||
if (!requirementsSection) {
|
||||
throw new Error('Spec must have a Requirements section');
|
||||
}
|
||||
|
||||
const requirements = this.parseRequirements(requirementsSection);
|
||||
|
||||
return {
|
||||
name,
|
||||
overview: purpose.trim(),
|
||||
requirements,
|
||||
metadata: {
|
||||
version: '1.0.0',
|
||||
format: 'openspec',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
parseChange(name: string): Change {
|
||||
const sections = this.parseSections();
|
||||
const why = this.findSection(sections, 'Why')?.content || '';
|
||||
const whatChanges = this.findSection(sections, 'What Changes')?.content || '';
|
||||
|
||||
if (!why) {
|
||||
throw new Error('Change must have a Why section');
|
||||
}
|
||||
|
||||
if (!whatChanges) {
|
||||
throw new Error('Change must have a What Changes section');
|
||||
}
|
||||
|
||||
const deltas = this.parseDeltas(whatChanges);
|
||||
|
||||
return {
|
||||
name,
|
||||
why: why.trim(),
|
||||
whatChanges: whatChanges.trim(),
|
||||
deltas,
|
||||
metadata: {
|
||||
version: '1.0.0',
|
||||
format: 'openspec-change',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected parseSections(): Section[] {
|
||||
const sections: Section[] = [];
|
||||
const stack: Section[] = [];
|
||||
|
||||
for (let i = 0; i < this.lines.length; i++) {
|
||||
const line = this.lines[i];
|
||||
const headerMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
||||
|
||||
if (headerMatch) {
|
||||
const level = headerMatch[1].length;
|
||||
const title = headerMatch[2].trim();
|
||||
const content = this.getContentUntilNextHeader(i + 1, level);
|
||||
|
||||
const section: Section = {
|
||||
level,
|
||||
title,
|
||||
content,
|
||||
children: [],
|
||||
};
|
||||
|
||||
while (stack.length > 0 && stack[stack.length - 1].level >= level) {
|
||||
stack.pop();
|
||||
}
|
||||
|
||||
if (stack.length === 0) {
|
||||
sections.push(section);
|
||||
} else {
|
||||
stack[stack.length - 1].children.push(section);
|
||||
}
|
||||
|
||||
stack.push(section);
|
||||
}
|
||||
}
|
||||
|
||||
return sections;
|
||||
}
|
||||
|
||||
protected getContentUntilNextHeader(startLine: number, currentLevel: number): string {
|
||||
const contentLines: string[] = [];
|
||||
|
||||
for (let i = startLine; i < this.lines.length; i++) {
|
||||
const line = this.lines[i];
|
||||
const headerMatch = line.match(/^(#{1,6})\s+/);
|
||||
|
||||
if (headerMatch && headerMatch[1].length <= currentLevel) {
|
||||
break;
|
||||
}
|
||||
|
||||
contentLines.push(line);
|
||||
}
|
||||
|
||||
return contentLines.join('\n').trim();
|
||||
}
|
||||
|
||||
protected findSection(sections: Section[], title: string): Section | undefined {
|
||||
for (const section of sections) {
|
||||
if (section.title.toLowerCase() === title.toLowerCase()) {
|
||||
return section;
|
||||
}
|
||||
const child = this.findSection(section.children, title);
|
||||
if (child) {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected parseRequirements(section: Section): Requirement[] {
|
||||
const requirements: Requirement[] = [];
|
||||
|
||||
for (const child of section.children) {
|
||||
// Extract requirement text from first non-empty content line, fall back to heading
|
||||
let text = child.title;
|
||||
|
||||
// Get content before any child sections (scenarios)
|
||||
if (child.content.trim()) {
|
||||
// Split content into lines and find content before any child headers
|
||||
const lines = child.content.split('\n');
|
||||
const contentBeforeChildren: string[] = [];
|
||||
|
||||
for (const line of lines) {
|
||||
// Stop at child headers (scenarios start with ####)
|
||||
if (line.trim().startsWith('#')) {
|
||||
break;
|
||||
}
|
||||
contentBeforeChildren.push(line);
|
||||
}
|
||||
|
||||
// Find first non-empty line
|
||||
const directContent = contentBeforeChildren.join('\n').trim();
|
||||
if (directContent) {
|
||||
const firstLine = directContent.split('\n').find(l => l.trim());
|
||||
if (firstLine) {
|
||||
text = firstLine.trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const scenarios = this.parseScenarios(child);
|
||||
|
||||
requirements.push({
|
||||
text,
|
||||
scenarios,
|
||||
});
|
||||
}
|
||||
|
||||
return requirements;
|
||||
}
|
||||
|
||||
protected parseScenarios(requirementSection: Section): Scenario[] {
|
||||
const scenarios: Scenario[] = [];
|
||||
|
||||
for (const scenarioSection of requirementSection.children) {
|
||||
// Store the raw text content of the scenario section
|
||||
if (scenarioSection.content.trim()) {
|
||||
scenarios.push({
|
||||
rawText: scenarioSection.content
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return scenarios;
|
||||
}
|
||||
|
||||
|
||||
protected parseDeltas(content: string): Delta[] {
|
||||
const deltas: Delta[] = [];
|
||||
const lines = content.split('\n');
|
||||
|
||||
for (const line of lines) {
|
||||
// Match both formats: **spec:** and **spec**:
|
||||
const deltaMatch = line.match(/^\s*-\s*\*\*([^*:]+)(?::\*\*|\*\*:)\s*(.+)$/);
|
||||
if (deltaMatch) {
|
||||
const specName = deltaMatch[1].trim();
|
||||
const description = deltaMatch[2].trim();
|
||||
|
||||
let operation: DeltaOperation = 'MODIFIED';
|
||||
const lowerDesc = description.toLowerCase();
|
||||
|
||||
// Use word boundaries to avoid false matches (e.g., "address" matching "add")
|
||||
// Check RENAMED first since it's more specific than patterns containing "new"
|
||||
if (/\brename(s|d|ing)?\b/.test(lowerDesc) || /\brenamed\s+(to|from)\b/.test(lowerDesc)) {
|
||||
operation = 'RENAMED';
|
||||
} else if (/\badd(s|ed|ing)?\b/.test(lowerDesc) || /\bcreate(s|d|ing)?\b/.test(lowerDesc) || /\bnew\b/.test(lowerDesc)) {
|
||||
operation = 'ADDED';
|
||||
} else if (/\bremove(s|d|ing)?\b/.test(lowerDesc) || /\bdelete(s|d|ing)?\b/.test(lowerDesc)) {
|
||||
operation = 'REMOVED';
|
||||
}
|
||||
|
||||
deltas.push({
|
||||
spec: specName,
|
||||
operation,
|
||||
description,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return deltas;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
export interface RequirementBlock {
|
||||
headerLine: string; // e.g., '### Requirement: Something'
|
||||
name: string; // e.g., 'Something'
|
||||
raw: string; // full block including headerLine and following content
|
||||
}
|
||||
|
||||
export interface RequirementsSectionParts {
|
||||
before: string;
|
||||
headerLine: string; // the '## Requirements' line
|
||||
preamble: string; // content between headerLine and first requirement block
|
||||
bodyBlocks: RequirementBlock[]; // parsed requirement blocks in order
|
||||
after: string;
|
||||
}
|
||||
|
||||
export function normalizeRequirementName(name: string): string {
|
||||
return name.trim();
|
||||
}
|
||||
|
||||
const REQUIREMENT_HEADER_REGEX = /^###\s*Requirement:\s*(.+)\s*$/;
|
||||
|
||||
/**
|
||||
* Extracts the Requirements section from a spec file and parses requirement blocks.
|
||||
*/
|
||||
export function extractRequirementsSection(content: string): RequirementsSectionParts {
|
||||
const lines = content.split('\n');
|
||||
const reqHeaderIndex = lines.findIndex(l => /^##\s+Requirements\s*$/i.test(l));
|
||||
|
||||
if (reqHeaderIndex === -1) {
|
||||
// No requirements section; create an empty one at the end
|
||||
const before = content.trimEnd();
|
||||
const headerLine = '## Requirements';
|
||||
return {
|
||||
before: before ? before + '\n\n' : '',
|
||||
headerLine,
|
||||
preamble: '',
|
||||
bodyBlocks: [],
|
||||
after: '\n',
|
||||
};
|
||||
}
|
||||
|
||||
// Find end of this section: next line that starts with '## ' at same or higher level
|
||||
let endIndex = lines.length;
|
||||
for (let i = reqHeaderIndex + 1; i < lines.length; i++) {
|
||||
if (/^##\s+/.test(lines[i])) {
|
||||
endIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const before = lines.slice(0, reqHeaderIndex).join('\n');
|
||||
const headerLine = lines[reqHeaderIndex];
|
||||
const sectionBodyLines = lines.slice(reqHeaderIndex + 1, endIndex);
|
||||
|
||||
// Parse requirement blocks within section body
|
||||
const blocks: RequirementBlock[] = [];
|
||||
let cursor = 0;
|
||||
let preambleLines: string[] = [];
|
||||
|
||||
// Collect preamble lines until first requirement header
|
||||
while (cursor < sectionBodyLines.length && !/^###\s+Requirement:/.test(sectionBodyLines[cursor])) {
|
||||
preambleLines.push(sectionBodyLines[cursor]);
|
||||
cursor++;
|
||||
}
|
||||
|
||||
while (cursor < sectionBodyLines.length) {
|
||||
const headerStart = cursor;
|
||||
const headerLineCandidate = sectionBodyLines[cursor];
|
||||
const headerMatch = headerLineCandidate.match(REQUIREMENT_HEADER_REGEX);
|
||||
if (!headerMatch) {
|
||||
// Not a requirement header; skip line defensively
|
||||
cursor++;
|
||||
continue;
|
||||
}
|
||||
const name = normalizeRequirementName(headerMatch[1]);
|
||||
cursor++;
|
||||
// Gather lines until next requirement header or end of section
|
||||
const bodyLines: string[] = [headerLineCandidate];
|
||||
while (cursor < sectionBodyLines.length && !/^###\s+Requirement:/.test(sectionBodyLines[cursor]) && !/^##\s+/.test(sectionBodyLines[cursor])) {
|
||||
bodyLines.push(sectionBodyLines[cursor]);
|
||||
cursor++;
|
||||
}
|
||||
const raw = bodyLines.join('\n').trimEnd();
|
||||
blocks.push({ headerLine: headerLineCandidate, name, raw });
|
||||
}
|
||||
|
||||
const after = lines.slice(endIndex).join('\n');
|
||||
const preamble = preambleLines.join('\n').trimEnd();
|
||||
|
||||
return {
|
||||
before: before.trimEnd() ? before + '\n' : before,
|
||||
headerLine,
|
||||
preamble,
|
||||
bodyBlocks: blocks,
|
||||
after: after.startsWith('\n') ? after : '\n' + after,
|
||||
};
|
||||
}
|
||||
|
||||
export interface DeltaPlan {
|
||||
added: RequirementBlock[];
|
||||
modified: RequirementBlock[];
|
||||
removed: string[]; // requirement names
|
||||
renamed: Array<{ from: string; to: string }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a delta-formatted spec change file content into a DeltaPlan with raw blocks.
|
||||
*/
|
||||
export function parseDeltaSpec(content: string): DeltaPlan {
|
||||
const sections = splitTopLevelSections(content);
|
||||
const added = parseRequirementBlocksFromSection(sections['ADDED Requirements'] || '');
|
||||
const modified = parseRequirementBlocksFromSection(sections['MODIFIED Requirements'] || '');
|
||||
const removedNames = parseRemovedNames(sections['REMOVED Requirements'] || '');
|
||||
const renamedPairs = parseRenamedPairs(sections['RENAMED Requirements'] || '');
|
||||
return { added, modified, removed: removedNames, renamed: renamedPairs };
|
||||
}
|
||||
|
||||
function splitTopLevelSections(content: string): Record<string, string> {
|
||||
const lines = content.split('\n');
|
||||
const result: Record<string, string> = {};
|
||||
const indices: Array<{ title: string; index: number; level: number }> = [];
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const m = lines[i].match(/^(##)\s+(.+)$/);
|
||||
if (m) {
|
||||
const level = m[1].length; // only care for '##'
|
||||
indices.push({ title: m[2].trim(), index: i, level });
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < indices.length; i++) {
|
||||
const current = indices[i];
|
||||
const next = indices[i + 1];
|
||||
const body = lines.slice(current.index + 1, next ? next.index : lines.length).join('\n');
|
||||
result[current.title] = body;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function parseRequirementBlocksFromSection(sectionBody: string): RequirementBlock[] {
|
||||
if (!sectionBody) return [];
|
||||
const lines = sectionBody.split('\n');
|
||||
const blocks: RequirementBlock[] = [];
|
||||
let i = 0;
|
||||
while (i < lines.length) {
|
||||
// Seek next requirement header
|
||||
while (i < lines.length && !/^###\s+Requirement:/.test(lines[i])) i++;
|
||||
if (i >= lines.length) break;
|
||||
const headerLine = lines[i];
|
||||
const m = headerLine.match(REQUIREMENT_HEADER_REGEX);
|
||||
if (!m) { i++; continue; }
|
||||
const name = normalizeRequirementName(m[1]);
|
||||
const buf: string[] = [headerLine];
|
||||
i++;
|
||||
while (i < lines.length && !/^###\s+Requirement:/.test(lines[i]) && !/^##\s+/.test(lines[i])) {
|
||||
buf.push(lines[i]);
|
||||
i++;
|
||||
}
|
||||
blocks.push({ headerLine, name, raw: buf.join('\n').trimEnd() });
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
|
||||
function parseRemovedNames(sectionBody: string): string[] {
|
||||
if (!sectionBody) return [];
|
||||
const names: string[] = [];
|
||||
const lines = sectionBody.split('\n');
|
||||
for (const line of lines) {
|
||||
const m = line.match(REQUIREMENT_HEADER_REGEX);
|
||||
if (m) {
|
||||
names.push(normalizeRequirementName(m[1]));
|
||||
continue;
|
||||
}
|
||||
// Also support bullet list of headers
|
||||
const bullet = line.match(/^\s*-\s*`?###\s*Requirement:\s*(.+?)`?\s*$/);
|
||||
if (bullet) {
|
||||
names.push(normalizeRequirementName(bullet[1]));
|
||||
}
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
function parseRenamedPairs(sectionBody: string): Array<{ from: string; to: string }> {
|
||||
if (!sectionBody) return [];
|
||||
const pairs: Array<{ from: string; to: string }> = [];
|
||||
const lines = sectionBody.split('\n');
|
||||
let current: { from?: string; to?: string } = {};
|
||||
for (const line of lines) {
|
||||
const fromMatch = line.match(/^\s*-?\s*FROM:\s*`?###\s*Requirement:\s*(.+?)`?\s*$/);
|
||||
const toMatch = line.match(/^\s*-?\s*TO:\s*`?###\s*Requirement:\s*(.+?)`?\s*$/);
|
||||
if (fromMatch) {
|
||||
current.from = normalizeRequirementName(fromMatch[1]);
|
||||
} else if (toMatch) {
|
||||
current.to = normalizeRequirementName(toMatch[1]);
|
||||
if (current.from && current.to) {
|
||||
pairs.push({ from: current.from, to: current.to });
|
||||
current = {};
|
||||
}
|
||||
}
|
||||
}
|
||||
return pairs;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { z } from 'zod';
|
||||
import { VALIDATION_MESSAGES } from '../validation/constants.js';
|
||||
|
||||
export const ScenarioSchema = z.object({
|
||||
rawText: z.string().min(1, VALIDATION_MESSAGES.SCENARIO_EMPTY),
|
||||
});
|
||||
|
||||
export const RequirementSchema = z.object({
|
||||
text: z.string()
|
||||
.min(1, VALIDATION_MESSAGES.REQUIREMENT_EMPTY)
|
||||
.refine(
|
||||
(text) => text.includes('SHALL') || text.includes('MUST'),
|
||||
VALIDATION_MESSAGES.REQUIREMENT_NO_SHALL
|
||||
),
|
||||
scenarios: z.array(ScenarioSchema)
|
||||
.min(1, VALIDATION_MESSAGES.REQUIREMENT_NO_SCENARIOS),
|
||||
});
|
||||
|
||||
export type Scenario = z.infer<typeof ScenarioSchema>;
|
||||
export type Requirement = z.infer<typeof RequirementSchema>;
|
||||
@@ -0,0 +1,42 @@
|
||||
import { z } from 'zod';
|
||||
import { RequirementSchema } from './base.schema.js';
|
||||
import {
|
||||
MIN_WHY_SECTION_LENGTH,
|
||||
MAX_WHY_SECTION_LENGTH,
|
||||
MAX_DELTAS_PER_CHANGE,
|
||||
VALIDATION_MESSAGES
|
||||
} from '../validation/constants.js';
|
||||
|
||||
export const DeltaOperationType = z.enum(['ADDED', 'MODIFIED', 'REMOVED', 'RENAMED']);
|
||||
|
||||
export const DeltaSchema = z.object({
|
||||
spec: z.string().min(1, VALIDATION_MESSAGES.DELTA_SPEC_EMPTY),
|
||||
operation: DeltaOperationType,
|
||||
description: z.string().min(1, VALIDATION_MESSAGES.DELTA_DESCRIPTION_EMPTY),
|
||||
requirement: RequirementSchema.optional(),
|
||||
requirements: z.array(RequirementSchema).optional(),
|
||||
rename: z.object({
|
||||
from: z.string(),
|
||||
to: z.string(),
|
||||
}).optional(),
|
||||
});
|
||||
|
||||
export const ChangeSchema = z.object({
|
||||
name: z.string().min(1, VALIDATION_MESSAGES.CHANGE_NAME_EMPTY),
|
||||
why: z.string()
|
||||
.min(MIN_WHY_SECTION_LENGTH, VALIDATION_MESSAGES.CHANGE_WHY_TOO_SHORT)
|
||||
.max(MAX_WHY_SECTION_LENGTH, VALIDATION_MESSAGES.CHANGE_WHY_TOO_LONG),
|
||||
whatChanges: z.string().min(1, VALIDATION_MESSAGES.CHANGE_WHAT_EMPTY),
|
||||
deltas: z.array(DeltaSchema)
|
||||
.min(1, VALIDATION_MESSAGES.CHANGE_NO_DELTAS)
|
||||
.max(MAX_DELTAS_PER_CHANGE, VALIDATION_MESSAGES.CHANGE_TOO_MANY_DELTAS),
|
||||
metadata: z.object({
|
||||
version: z.string().default('1.0.0'),
|
||||
format: z.literal('openspec-change'),
|
||||
sourcePath: z.string().optional(),
|
||||
}).optional(),
|
||||
});
|
||||
|
||||
export type DeltaOperation = z.infer<typeof DeltaOperationType>;
|
||||
export type Delta = z.infer<typeof DeltaSchema>;
|
||||
export type Change = z.infer<typeof ChangeSchema>;
|
||||
@@ -0,0 +1,20 @@
|
||||
export {
|
||||
ScenarioSchema,
|
||||
RequirementSchema,
|
||||
type Scenario,
|
||||
type Requirement,
|
||||
} from './base.schema.js';
|
||||
|
||||
export {
|
||||
SpecSchema,
|
||||
type Spec,
|
||||
} from './spec.schema.js';
|
||||
|
||||
export {
|
||||
DeltaOperationType,
|
||||
DeltaSchema,
|
||||
ChangeSchema,
|
||||
type DeltaOperation,
|
||||
type Delta,
|
||||
type Change,
|
||||
} from './change.schema.js';
|
||||
@@ -0,0 +1,17 @@
|
||||
import { z } from 'zod';
|
||||
import { RequirementSchema } from './base.schema.js';
|
||||
import { VALIDATION_MESSAGES } from '../validation/constants.js';
|
||||
|
||||
export const SpecSchema = z.object({
|
||||
name: z.string().min(1, VALIDATION_MESSAGES.SPEC_NAME_EMPTY),
|
||||
overview: z.string().min(1, VALIDATION_MESSAGES.SPEC_PURPOSE_EMPTY),
|
||||
requirements: z.array(RequirementSchema)
|
||||
.min(1, VALIDATION_MESSAGES.SPEC_NO_REQUIREMENTS),
|
||||
metadata: z.object({
|
||||
version: z.string().default('1.0.0'),
|
||||
format: z.literal('openspec'),
|
||||
sourcePath: z.string().optional(),
|
||||
}).optional(),
|
||||
});
|
||||
|
||||
export type Spec = z.infer<typeof SpecSchema>;
|
||||
@@ -0,0 +1,26 @@
|
||||
export const claudeTemplate = `# OpenSpec Project
|
||||
|
||||
This document provides instructions for AI coding assistants on how to use OpenSpec conventions for spec-driven development. Follow these rules precisely when working on OpenSpec-enabled projects.
|
||||
|
||||
This project uses OpenSpec for spec-driven development. Specifications are the source of truth.
|
||||
|
||||
See @openspec/README.md for detailed conventions and guidelines.
|
||||
|
||||
## Complexity Management
|
||||
|
||||
**Default to minimal solutions:**
|
||||
- Propose <100 lines of new code for features
|
||||
- Prefer single-file implementations until proven insufficient
|
||||
- Avoid frameworks, abstractions, and optimizations without clear justification
|
||||
- Choose boring, well-understood patterns over novel approaches
|
||||
|
||||
**Question requests for complexity:**
|
||||
- Caching? → Ask for performance data and targets
|
||||
- New framework? → Suggest plain code first
|
||||
- Extra layers? → Start with the thinnest viable design
|
||||
|
||||
**Justify complexity with data:**
|
||||
- Performance metrics showing current solution is too slow
|
||||
- Concrete scale requirements (e.g., >1000 users, >100MB data)
|
||||
- Multiple proven use cases requiring an abstraction
|
||||
`;
|
||||
@@ -0,0 +1,29 @@
|
||||
import { readmeTemplate } from './readme-template.js';
|
||||
import { projectTemplate, ProjectContext } from './project-template.js';
|
||||
import { claudeTemplate } from './claude-template.js';
|
||||
|
||||
export interface Template {
|
||||
path: string;
|
||||
content: string | ((context: ProjectContext) => string);
|
||||
}
|
||||
|
||||
export class TemplateManager {
|
||||
static getTemplates(context: ProjectContext = {}): Template[] {
|
||||
return [
|
||||
{
|
||||
path: 'README.md',
|
||||
content: readmeTemplate
|
||||
},
|
||||
{
|
||||
path: 'project.md',
|
||||
content: projectTemplate(context)
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
static getClaudeTemplate(): string {
|
||||
return claudeTemplate;
|
||||
}
|
||||
}
|
||||
|
||||
export { ProjectContext } from './project-template.js';
|
||||
@@ -0,0 +1,38 @@
|
||||
export interface ProjectContext {
|
||||
projectName?: string;
|
||||
description?: string;
|
||||
techStack?: string[];
|
||||
conventions?: string;
|
||||
}
|
||||
|
||||
export const projectTemplate = (context: ProjectContext = {}) => `# ${context.projectName || 'Project'} Context
|
||||
|
||||
## Purpose
|
||||
${context.description || '[Describe your project\'s purpose and goals]'}
|
||||
|
||||
## Tech Stack
|
||||
${context.techStack?.length ? context.techStack.map(tech => `- ${tech}`).join('\n') : '- [List your primary technologies]\n- [e.g., TypeScript, React, Node.js]'}
|
||||
|
||||
## Project Conventions
|
||||
|
||||
### Code Style
|
||||
[Describe your code style preferences, formatting rules, and naming conventions]
|
||||
|
||||
### Architecture Patterns
|
||||
[Document your architectural decisions and patterns]
|
||||
|
||||
### Testing Strategy
|
||||
[Explain your testing approach and requirements]
|
||||
|
||||
### Git Workflow
|
||||
[Describe your branching strategy and commit conventions]
|
||||
|
||||
## Domain Context
|
||||
[Add domain-specific knowledge that AI assistants need to understand]
|
||||
|
||||
## Important Constraints
|
||||
[List any technical, business, or regulatory constraints]
|
||||
|
||||
## External Dependencies
|
||||
[Document key external services, APIs, or systems]
|
||||
`;
|
||||
@@ -0,0 +1,518 @@
|
||||
export const readmeTemplate = `# OpenSpec Instructions
|
||||
|
||||
This document provides instructions for AI coding assistants on how to use OpenSpec conventions for spec-driven development. Follow these rules precisely when working on OpenSpec-enabled projects.
|
||||
|
||||
## Core Principle
|
||||
|
||||
OpenSpec is an AI-native system for change-driven development where:
|
||||
- **Specs** (\`specs/\`) reflect what IS currently built and deployed
|
||||
- **Changes** (\`changes/\`) contain proposals for what SHOULD be changed
|
||||
- **AI drives the process** - You generate proposals, humans review and approve
|
||||
- **Specs are living documentation** - Always kept in sync with deployed code
|
||||
|
||||
## Start Simple
|
||||
|
||||
**Default to minimal implementations:**
|
||||
- New features should be <100 lines of code initially
|
||||
- Use the simplest solution that works
|
||||
- Avoid premature optimization (no caching, parallelization, or complex patterns without proven need)
|
||||
- Choose boring technology over cutting-edge solutions
|
||||
|
||||
**Complexity triggers** - Only add complexity when you have:
|
||||
- **Performance data** showing current solution is too slow
|
||||
- **Scale requirements** with specific numbers (>1000 users, >100MB data)
|
||||
- **Multiple use cases** requiring the same abstraction
|
||||
- **Regulatory compliance** mandating specific patterns
|
||||
- **Security threats** that simple solutions cannot address
|
||||
|
||||
When triggered, document the specific justification in your change proposal.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
\`\`\`
|
||||
openspec/
|
||||
├── project.md # Project-specific context (tech stack, conventions)
|
||||
├── README.md # This file - OpenSpec instructions
|
||||
├── specs/ # Current truth - what IS built
|
||||
│ ├── [capability]/ # Single, focused capability
|
||||
│ │ ├── spec.md # WHAT the capability does and WHY
|
||||
│ │ └── design.md # HOW it's built (established patterns)
|
||||
│ └── ...
|
||||
├── changes/ # Proposed changes - what we're CHANGING
|
||||
│ ├── [change-name]/
|
||||
│ │ ├── proposal.md # Why, what, impact (consolidated)
|
||||
│ │ ├── tasks.md # Implementation checklist
|
||||
│ │ ├── design.md # Technical decisions (optional, for complex changes)
|
||||
│ │ └── specs/ # Delta changes to specs
|
||||
│ │ └── [capability]/
|
||||
│ │ └── spec.md # Delta format (ADDED/MODIFIED/REMOVED/RENAMED)
|
||||
│ └── archive/ # Completed changes (dated)
|
||||
\`\`\`
|
||||
|
||||
### Capability Organization
|
||||
|
||||
**Use capabilities, not features** - Each directory under \`specs/\` represents a single, focused responsibility:
|
||||
- **Verb-noun naming**: \`user-auth\`, \`payment-capture\`, \`order-checkout\`
|
||||
- **10-minute rule**: Each capability should be understandable in <10 minutes
|
||||
- **Single purpose**: If it needs "AND" to describe it, split it
|
||||
|
||||
Examples:
|
||||
\`\`\`
|
||||
✅ GOOD: user-auth, user-sessions, payment-capture, payment-refunds
|
||||
❌ BAD: users, payments, core, misc
|
||||
\`\`\`
|
||||
|
||||
## Key Behavioral Rules
|
||||
|
||||
### 1. Always Start by Reading
|
||||
|
||||
Before any task:
|
||||
1. **Read relevant specs** in \`specs/[capability]/spec.md\` to understand current state
|
||||
2. **Check pending changes** in \`changes/\` directory for potential conflicts
|
||||
3. **Read project.md** for project-specific conventions
|
||||
|
||||
### 2. When to Create Change Proposals
|
||||
|
||||
**ALWAYS create a change proposal for:**
|
||||
- New features or functionality
|
||||
- Breaking changes (API changes, schema updates)
|
||||
- Architecture changes or new patterns
|
||||
- Performance optimizations that change behavior
|
||||
- Security updates affecting auth/access patterns
|
||||
- Any change requiring multiple steps or affecting multiple systems
|
||||
|
||||
**SKIP proposals for:**
|
||||
- Bug fixes that restore intended behavior
|
||||
- Typos, formatting, or comment updates
|
||||
- Dependency updates (unless breaking)
|
||||
- Configuration or environment variable changes
|
||||
- Adding tests for existing behavior
|
||||
- Documentation fixes
|
||||
|
||||
**Complexity assessment:**
|
||||
- If your solution requires >100 lines of new code, justify the complexity
|
||||
- If adding dependencies, frameworks, or architectural patterns, document why simpler alternatives won't work
|
||||
- Default to single-file implementations until proven insufficient
|
||||
|
||||
### 3. Delta-Based Change Format
|
||||
|
||||
Changes use a delta format with clear sections:
|
||||
|
||||
\`\`\`markdown
|
||||
## ADDED Requirements
|
||||
### Requirement: New Feature
|
||||
[Complete requirement content in structured format]
|
||||
|
||||
## MODIFIED Requirements
|
||||
### Requirement: Existing Feature
|
||||
[Complete modified requirement (header must match current spec)]
|
||||
|
||||
## REMOVED Requirements
|
||||
### Requirement: Old Feature
|
||||
**Reason for removal**: [Why removing]
|
||||
**Migration path**: [How to handle existing usage]
|
||||
|
||||
## RENAMED Requirements
|
||||
- FROM: \`### Requirement: Old Name\`
|
||||
- TO: \`### Requirement: New Name\`
|
||||
\`\`\`
|
||||
|
||||
Key rules:
|
||||
- Headers are matched using \`normalize(header) = trim(header)\`
|
||||
- Include complete requirements (not diffs)
|
||||
- Use standard symbols in CLI output: + (added), ~ (modified), - (removed), → (renamed)
|
||||
|
||||
### 4. Creating a Change Proposal
|
||||
|
||||
When a user requests a significant change:
|
||||
|
||||
\`\`\`bash
|
||||
# 1. Create the change directory
|
||||
openspec/changes/[descriptive-name]/
|
||||
|
||||
# 2. Generate proposal.md with all context
|
||||
## Why
|
||||
[1-2 sentences on the problem/opportunity]
|
||||
|
||||
## What Changes
|
||||
[Bullet list of changes, including breaking changes]
|
||||
|
||||
## Impact
|
||||
- Affected specs: [list capabilities that will change]
|
||||
- Affected code: [list key files/systems]
|
||||
|
||||
# 3. Create delta specs for ALL affected capabilities
|
||||
# - Store only the changes (not complete future state)
|
||||
# - Use sections: ## ADDED, ## MODIFIED, ## REMOVED, ## RENAMED
|
||||
# - Include complete requirements in their final form
|
||||
# Example spec.md content:
|
||||
# ## ADDED Requirements
|
||||
# ### Requirement: Password Reset
|
||||
# Users SHALL be able to reset passwords via email...
|
||||
#
|
||||
# ## MODIFIED Requirements
|
||||
# ### Requirement: User Authentication
|
||||
# [Complete modified requirement with new password reset hook]
|
||||
specs/
|
||||
└── [capability]/
|
||||
└── spec.md # Contains delta sections
|
||||
|
||||
# 4. Create tasks.md with implementation steps
|
||||
## 1. [Task Group]
|
||||
- [ ] 1.1 [Specific task]
|
||||
- [ ] 1.2 [Specific task]
|
||||
|
||||
# 5. For complex changes, add design.md
|
||||
[Technical decisions and trade-offs]
|
||||
\`\`\`
|
||||
|
||||
### 5. The Change Lifecycle
|
||||
|
||||
1. **Propose** → Create change directory with delta-based documentation
|
||||
2. **Review** → User reviews and approves the proposal
|
||||
3. **Implement** → Follow the approved tasks.md (can be multiple PRs)
|
||||
4. **Deploy** → User confirms deployment
|
||||
5. **Update Specs** → Apply deltas to sync specs/ with new reality (IF the change affects system capabilities)
|
||||
6. **Archive** → Move to \`changes/archive/YYYY-MM-DD-[name]/\`
|
||||
|
||||
### 6. Implementing Changes
|
||||
|
||||
When implementing an approved change:
|
||||
1. Follow the tasks.md checklist exactly
|
||||
2. **Mark completed tasks** in tasks.md as you finish them (e.g., \`- [x] 1.1 Task completed\`)
|
||||
3. Ensure code matches the proposed behavior
|
||||
4. Update any affected tests
|
||||
5. **Keep change in \`changes/\` directory** - do NOT archive in implementation PR
|
||||
|
||||
**Multiple Implementation PRs:**
|
||||
- Changes can be implemented across multiple PRs
|
||||
- Each PR should update tasks.md to mark what was completed
|
||||
- Different developers can work on different task groups
|
||||
- Example: PR #1 completes tasks 1.1-1.3, PR #2 completes tasks 2.1-2.4
|
||||
|
||||
### 7. Updating Specs and Archiving After Deployment
|
||||
|
||||
**Create a separate PR after deployment** that:
|
||||
1. Moves change to \`changes/archive/YYYY-MM-DD-[name]/\`
|
||||
2. Updates relevant files in \`specs/\` to reflect new reality (if needed)
|
||||
3. If design.md exists, incorporates proven patterns into \`specs/[capability]/design.md\`
|
||||
|
||||
This ensures changes are only archived when truly complete and deployed.
|
||||
|
||||
### 8. Types of Changes That Don't Require Specs
|
||||
|
||||
Some changes only affect development infrastructure and don't need specs:
|
||||
- Initial project setup (package.json, tsconfig.json, etc.)
|
||||
- Development tooling changes (linters, formatters, build tools)
|
||||
- CI/CD configuration
|
||||
- Development dependencies
|
||||
|
||||
For these changes:
|
||||
1. Implement → Deploy → Mark tasks complete → Archive
|
||||
2. Skip the "Update Specs" step entirely
|
||||
|
||||
### What Deserves a Spec?
|
||||
|
||||
Ask yourself:
|
||||
- Is this a system capability that users or other systems interact with?
|
||||
- Does it have ongoing behavior that needs documentation?
|
||||
- Would a new developer need to understand this to work with the system?
|
||||
|
||||
If NO to all → No spec needed (likely just tooling/infrastructure)
|
||||
|
||||
## Understanding Specs vs Code
|
||||
|
||||
### Specs Document WHAT and WHY
|
||||
\`\`\`markdown
|
||||
# Authentication Spec
|
||||
|
||||
Users SHALL authenticate with email and password.
|
||||
|
||||
WHEN credentials are valid THEN issue JWT token.
|
||||
WHEN credentials are invalid THEN return generic error.
|
||||
|
||||
WHY: Prevent user enumeration attacks.
|
||||
\`\`\`
|
||||
|
||||
### Code Documents HOW
|
||||
\`\`\`javascript
|
||||
// Implementation details
|
||||
const user = await db.users.findOne({ email });
|
||||
const valid = await bcrypt.compare(password, user.hashedPassword);
|
||||
\`\`\`
|
||||
|
||||
**Key Distinction**: Specs capture intent, constraints, and decisions that aren't obvious from code.
|
||||
|
||||
## Common Scenarios
|
||||
|
||||
### New Feature Request
|
||||
\`\`\`
|
||||
User: "Add password reset functionality"
|
||||
|
||||
You should:
|
||||
1. Read specs/user-auth/spec.md
|
||||
2. Check changes/ for pending auth changes
|
||||
3. Create changes/add-password-reset/ with:
|
||||
- proposal.md describing the change
|
||||
- specs/user-auth/spec.md with:
|
||||
## ADDED Requirements
|
||||
### Requirement: Password Reset
|
||||
[Complete requirement for password reset]
|
||||
|
||||
## MODIFIED Requirements
|
||||
### Requirement: User Authentication
|
||||
[Updated to integrate with password reset]
|
||||
4. Wait for approval before implementing
|
||||
\`\`\`
|
||||
|
||||
### Bug Fix
|
||||
\`\`\`
|
||||
User: "Getting null pointer error when bio is empty"
|
||||
|
||||
You should:
|
||||
1. Check if spec says bios are optional
|
||||
2. If yes → Fix directly (it's a bug)
|
||||
3. If no → Create change proposal (it's a behavior change)
|
||||
\`\`\`
|
||||
|
||||
### Infrastructure Setup
|
||||
\`\`\`
|
||||
User: "Initialize TypeScript project"
|
||||
|
||||
You should:
|
||||
1. Create change proposal for TypeScript setup
|
||||
2. Implement configuration files (PR #1)
|
||||
3. Mark tasks complete in tasks.md
|
||||
4. After deployment, create separate PR to archive
|
||||
(no specs update needed - this is tooling, not a capability)
|
||||
\`\`\`
|
||||
|
||||
## Summary Workflow
|
||||
|
||||
1. **Receive request** → Determine if it needs a change proposal
|
||||
2. **Read current state** → Check specs and pending changes
|
||||
3. **Create proposal** → Generate complete change documentation
|
||||
4. **Get approval** → User reviews the proposal
|
||||
5. **Implement** → Follow approved tasks, mark completed items in tasks.md
|
||||
6. **Deploy** → User deploys the implementation
|
||||
7. **Archive PR** → Create separate PR to:
|
||||
- Move change to archive
|
||||
- Update specs if needed
|
||||
- Mark change as complete
|
||||
|
||||
## PR Workflow Examples
|
||||
|
||||
### Single Developer, Simple Change
|
||||
\`\`\`
|
||||
PR #1: Implementation
|
||||
- Implement all tasks
|
||||
- Update tasks.md marking items complete
|
||||
- Get merged and deployed
|
||||
|
||||
PR #2: Archive (after deployment)
|
||||
- Move changes/feature-x/ → changes/archive/2025-01-15-feature-x/
|
||||
- Update specs if needed
|
||||
\`\`\`
|
||||
|
||||
### Multiple Developers, Complex Change
|
||||
\`\`\`
|
||||
PR #1: Alice implements auth components
|
||||
- Complete tasks 1.1, 1.2, 1.3
|
||||
- Update tasks.md marking these complete
|
||||
|
||||
PR #2: Bob implements UI components
|
||||
- Complete tasks 2.1, 2.2
|
||||
- Update tasks.md marking these complete
|
||||
|
||||
PR #3: Alice fixes integration issues
|
||||
- Complete remaining task 1.4
|
||||
- Update tasks.md
|
||||
|
||||
[Deploy all changes]
|
||||
|
||||
PR #4: Archive
|
||||
- Move to archive with deployment date
|
||||
- Update specs to reflect new auth flow
|
||||
\`\`\`
|
||||
|
||||
### Key Rules
|
||||
- **Never archive in implementation PRs** - changes aren't done until deployed
|
||||
- **Always update tasks.md** - shows accurate progress
|
||||
- **One archive PR per change** - clear completion boundary
|
||||
- **Archive PR includes spec updates** - keeps specs current
|
||||
|
||||
## Capability Organization Best Practices
|
||||
|
||||
### Naming Capabilities
|
||||
- Use **verb-noun** patterns: \`user-auth\`, \`payment-capture\`, \`order-checkout\`
|
||||
- Be specific: \`payment-capture\` not just \`payments\`
|
||||
- Keep flat: Avoid nesting capabilities within capabilities
|
||||
- Singular focus: If you need "AND" to describe it, split it
|
||||
|
||||
### When to Split Capabilities
|
||||
Split when you have:
|
||||
- Multiple unrelated API endpoints
|
||||
- Different user personas or actors
|
||||
- Separate deployment considerations
|
||||
- Independent evolution paths
|
||||
|
||||
#### Capability Boundary Guidelines
|
||||
- Would you import these separately? → Separate capabilities
|
||||
- Different deployment cadence? → Separate capabilities
|
||||
- Different teams own them? → Separate capabilities
|
||||
- Shared data models are OK, shared business logic means combine
|
||||
|
||||
Examples:
|
||||
- user-auth (login/logout) vs user-sessions (token management) → SEPARATE
|
||||
- payment-capture vs payment-refunds → SEPARATE (different workflows)
|
||||
- user-profile vs user-settings → COMBINE (same data model, same owner)
|
||||
|
||||
### Cross-Cutting Concerns
|
||||
For system-wide policies (rate limiting, error handling, security), document them in:
|
||||
- \`project.md\` for project-wide conventions
|
||||
- Within relevant capability specs where they apply
|
||||
- Or create a dedicated capability if complex enough (e.g., \`api-rate-limiting/\`)
|
||||
|
||||
### Examples of Well-Organized Capabilities
|
||||
\`\`\`
|
||||
specs/
|
||||
├── user-auth/ # Login, logout, password reset
|
||||
├── user-sessions/ # Token management, refresh
|
||||
├── user-profile/ # Profile CRUD operations
|
||||
├── payment-capture/ # Processing payments
|
||||
├── payment-refunds/ # Handling refunds
|
||||
└── order-checkout/ # Checkout workflow
|
||||
\`\`\`
|
||||
|
||||
For detailed guidance, see the [Capability Organization Guide](../docs/capability-organization.md).
|
||||
|
||||
## Common Scenarios and Clarifications
|
||||
|
||||
### Decision Ambiguity: Bug vs Behavior Change
|
||||
|
||||
When specs are missing or ambiguous:
|
||||
- If NO spec exists → Treat current code behavior as implicit spec, require proposal
|
||||
- If spec is VAGUE → Require proposal to clarify spec alongside fix
|
||||
- If code and spec DISAGREE → Spec is truth, code is buggy (fix without proposal)
|
||||
- If unsure → Default to creating a proposal (safer option)
|
||||
|
||||
Example:
|
||||
\`\`\`
|
||||
User: "The API returns 404 for missing users but should return 400"
|
||||
AI: Is this a bug (spec says 400) or behavior change (spec says 404)?
|
||||
\`\`\`
|
||||
|
||||
### When You Don't Know the Scope
|
||||
It's OK to explore first! Tell the user you need to investigate, then create an informed proposal.
|
||||
|
||||
### Exploration Phase (When Needed)
|
||||
|
||||
BEFORE creating proposal, you may need exploration when:
|
||||
- User request is vague or high-level
|
||||
- Multiple implementation approaches exist
|
||||
- Scope is unclear without seeing code
|
||||
|
||||
Exploration checklist:
|
||||
1. Tell user you need to explore first
|
||||
2. Use Grep/Read to understand current state
|
||||
3. Create initial proposal based on findings
|
||||
4. Refine with user feedback
|
||||
|
||||
Example:
|
||||
\`\`\`
|
||||
User: "Add caching to improve performance"
|
||||
AI: "Let me explore the codebase to understand the current architecture and identify caching opportunities."
|
||||
[After exploration]
|
||||
AI: "Based on my analysis, I've identified three areas where caching would help. Here's my proposal..."
|
||||
\`\`\`
|
||||
|
||||
### When No Specs Exist
|
||||
Treat current code as implicit spec. Your proposal should document current state AND proposed changes.
|
||||
|
||||
### When in Doubt
|
||||
Default to creating a proposal. It's easier to skip an unnecessary proposal than fix an undocumented change.
|
||||
|
||||
### AI Workflow Adaptations
|
||||
|
||||
Task tracking with OpenSpec:
|
||||
- Track exploration tasks separately from implementation
|
||||
- Document proposal creation steps as you go
|
||||
- Keep implementation tasks separate until proposal approved
|
||||
|
||||
Parallel operations encouraged:
|
||||
- Read multiple specs simultaneously
|
||||
- Check multiple pending changes at once
|
||||
- Batch related searches for efficiency
|
||||
|
||||
Progress communication:
|
||||
- "Exploring codebase to understand scope..."
|
||||
- "Creating proposal based on findings..."
|
||||
- "Implementing approved changes..."
|
||||
|
||||
### For AI Assistants
|
||||
- **Bias toward simplicity** - Propose the minimal solution that works
|
||||
- Use your exploration tools liberally before proposing
|
||||
- Batch operations for efficiency
|
||||
- Communicate your progress
|
||||
- It's OK to revise proposals based on discoveries
|
||||
- **Question complexity** - If your solution feels complex, simplify first
|
||||
|
||||
## Edge Case Handling
|
||||
|
||||
### Multi-Capability Changes
|
||||
Create ONE proposal that:
|
||||
- Lists all affected capabilities
|
||||
- Shows changes per capability
|
||||
- Has unified task list
|
||||
- Gets approved as a whole
|
||||
|
||||
### Outdated Specs
|
||||
If specs clearly outdated:
|
||||
1. Create proposal to update specs to match reality
|
||||
2. Implement new feature in separate proposal
|
||||
3. OR combine both in one proposal with clear sections
|
||||
|
||||
### Emergency Hotfixes
|
||||
For critical production issues:
|
||||
1. Announce: "This is an emergency fix"
|
||||
2. Implement fix immediately
|
||||
3. Create retroactive proposal
|
||||
4. Update specs after deployment
|
||||
5. Tag with [EMERGENCY] in archive
|
||||
|
||||
### Pure Refactoring
|
||||
No proposal needed for:
|
||||
- Code formatting/style
|
||||
- Internal refactoring (same API)
|
||||
- Performance optimization (same behavior)
|
||||
- Adding types to untyped code
|
||||
|
||||
Proposal REQUIRED for:
|
||||
- API changes (even if compatible)
|
||||
- Database schema changes
|
||||
- Architecture changes
|
||||
- New dependencies
|
||||
|
||||
### Observability Additions
|
||||
No proposal needed for:
|
||||
- Adding log statements
|
||||
- New metrics/traces
|
||||
- Debugging additions
|
||||
- Error tracking
|
||||
|
||||
Proposal REQUIRED if:
|
||||
- Changes log format/structure
|
||||
- Adds new monitoring service
|
||||
- Changes what's logged (privacy)
|
||||
|
||||
## Remember
|
||||
|
||||
- You are the process driver - automate documentation burden
|
||||
- Specs must always reflect deployed reality
|
||||
- Changes are proposed, not imposed
|
||||
- Impact analysis prevents surprises
|
||||
- Simplicity is the power - just markdown files, minimal solutions
|
||||
- Start simple, add complexity only when justified
|
||||
|
||||
By following these conventions, you enable true spec-driven development where documentation stays current, changes are traceable, and evolution is intentional.
|
||||
`;
|
||||
@@ -0,0 +1,55 @@
|
||||
import path from 'path';
|
||||
import { FileSystemUtils } from '../utils/file-system.js';
|
||||
import { OPENSPEC_DIR_NAME } from './config.js';
|
||||
import { readmeTemplate } from './templates/readme-template.js';
|
||||
import { ToolRegistry } from './configurators/registry.js';
|
||||
|
||||
export class UpdateCommand {
|
||||
async execute(projectPath: string): Promise<void> {
|
||||
const resolvedProjectPath = path.resolve(projectPath);
|
||||
const openspecDirName = OPENSPEC_DIR_NAME;
|
||||
const openspecPath = path.join(resolvedProjectPath, openspecDirName);
|
||||
|
||||
// 1. Check openspec directory exists
|
||||
if (!await FileSystemUtils.directoryExists(openspecPath)) {
|
||||
throw new Error(`No OpenSpec directory found. Run 'openspec init' first.`);
|
||||
}
|
||||
|
||||
// 2. Update README.md (full replacement)
|
||||
const readmePath = path.join(openspecPath, 'README.md');
|
||||
await FileSystemUtils.writeFile(readmePath, readmeTemplate);
|
||||
|
||||
// 3. Update existing AI tool configuration files only
|
||||
const configurators = ToolRegistry.getAll();
|
||||
let updatedFiles: string[] = [];
|
||||
let failedFiles: string[] = [];
|
||||
|
||||
for (const configurator of configurators) {
|
||||
const configFilePath = path.join(resolvedProjectPath, configurator.configFileName);
|
||||
|
||||
// Only update if the file already exists
|
||||
if (await FileSystemUtils.fileExists(configFilePath)) {
|
||||
try {
|
||||
await configurator.configure(resolvedProjectPath, openspecPath);
|
||||
updatedFiles.push(configurator.configFileName);
|
||||
} catch (error) {
|
||||
failedFiles.push(configurator.configFileName);
|
||||
console.error(`Failed to update ${configurator.configFileName}: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Success message (ASCII-safe)
|
||||
const messages: string[] = ['Updated OpenSpec instructions (README.md)'];
|
||||
|
||||
if (updatedFiles.length > 0) {
|
||||
messages.push(`Updated AI tool files: ${updatedFiles.join(', ')}`);
|
||||
}
|
||||
|
||||
if (failedFiles.length > 0) {
|
||||
messages.push(`Failed to update: ${failedFiles.join(', ')}`);
|
||||
}
|
||||
|
||||
console.log(messages.join('\n'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Validation threshold constants
|
||||
*/
|
||||
|
||||
// Minimum character lengths
|
||||
export const MIN_WHY_SECTION_LENGTH = 50;
|
||||
export const MIN_PURPOSE_LENGTH = 50;
|
||||
|
||||
// Maximum character/item limits
|
||||
export const MAX_WHY_SECTION_LENGTH = 1000;
|
||||
export const MAX_REQUIREMENT_TEXT_LENGTH = 500;
|
||||
export const MAX_DELTAS_PER_CHANGE = 10;
|
||||
|
||||
// Validation messages
|
||||
export const VALIDATION_MESSAGES = {
|
||||
// Required content
|
||||
SCENARIO_EMPTY: 'Scenario text cannot be empty',
|
||||
REQUIREMENT_EMPTY: 'Requirement text cannot be empty',
|
||||
REQUIREMENT_NO_SHALL: 'Requirement must contain SHALL or MUST keyword',
|
||||
REQUIREMENT_NO_SCENARIOS: 'Requirement must have at least one scenario',
|
||||
SPEC_NAME_EMPTY: 'Spec name cannot be empty',
|
||||
SPEC_PURPOSE_EMPTY: 'Purpose section cannot be empty',
|
||||
SPEC_NO_REQUIREMENTS: 'Spec must have at least one requirement',
|
||||
CHANGE_NAME_EMPTY: 'Change name cannot be empty',
|
||||
CHANGE_WHY_TOO_SHORT: `Why section must be at least ${MIN_WHY_SECTION_LENGTH} characters`,
|
||||
CHANGE_WHY_TOO_LONG: `Why section should not exceed ${MAX_WHY_SECTION_LENGTH} characters`,
|
||||
CHANGE_WHAT_EMPTY: 'What Changes section cannot be empty',
|
||||
CHANGE_NO_DELTAS: 'Change must have at least one delta',
|
||||
CHANGE_TOO_MANY_DELTAS: `Consider splitting changes with more than ${MAX_DELTAS_PER_CHANGE} deltas`,
|
||||
DELTA_SPEC_EMPTY: 'Spec name cannot be empty',
|
||||
DELTA_DESCRIPTION_EMPTY: 'Delta description cannot be empty',
|
||||
|
||||
// Warnings
|
||||
PURPOSE_TOO_BRIEF: `Purpose section is too brief (less than ${MIN_PURPOSE_LENGTH} characters)`,
|
||||
REQUIREMENT_TOO_LONG: `Requirement text is very long (>${MAX_REQUIREMENT_TEXT_LENGTH} characters). Consider breaking it down.`,
|
||||
DELTA_DESCRIPTION_TOO_BRIEF: 'Delta description is too brief',
|
||||
DELTA_MISSING_REQUIREMENTS: 'Delta should include requirements',
|
||||
} as const;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user