Compare commits

...
Author SHA1 Message Date
Tabish Bidiwale e395eb4eeb feat: add list command to show active changes with task status 2025-08-13 17:17:40 +10:00
Tabish Bidiwale 76e1ec2a1f Merge pull request #12 from Fission-AI/feat/add-diff-command
feat: add diff command to view spec changes
2025-08-13 17:00:48 +10:00
Tabish Bidiwale 6cbb803e48 refactor: switch to jest-diff for better output and simpler code
- Replace custom diff implementation with jest-diff
- Reduce code from 229 to 178 lines (22% reduction)
- Get professional GitHub-style diff output
- Automatic word-level highlighting built-in
- Smaller bundle size (jest-diff: 85KB vs diff: 492KB)
2025-08-13 15:45:14 +10:00
Tabish Bidiwale 183b82f266 feat: enhance diff command with word-level highlighting and better formatting
- Add word-level diff highlighting for changed lines
- Improve file headers with status indicators and statistics
- Add summary view showing total files changed and lines modified
- Enhanced visual formatting with separators and colors
- Extract magic strings as constants for maintainability
2025-08-13 15:29:43 +10:00
Tabish Bidiwale 27eaccc024 Merge pull request #16 from Fission-AI/add-requirement-markers
feat: add @requirement markers convention
2025-08-13 15:28:56 +10:00
Tabish Bidiwale b288f2fc88 fix: update spec to contain complete future state per OpenSpec conventions 2025-08-13 15:21:14 +10:00
Tabish Bidiwale 22134a603b feat: add @requirement markers convention for requirement identification
- Define @requirement marker syntax for identifying requirements in specs
- Each marker includes a kebab-case identifier before WHEN/THEN blocks
- Document convention in openspec-conventions spec
- Enables reliable extraction without brittle regex parsing
2025-08-13 14:50:24 +10:00
Tabish Bidiwale 3b5fd11cb9 Merge pull request #13 from Fission-AI/feat/add-list-command
feat(openspec): add list command to display active changes
2025-08-12 16:57:43 +10:00
Tabish Bidiwale fce227a36e fix: address code review feedback for diff command
- Fix empty line filtering to preserve formatting in diffs
- Replace prompts with @inquirer/prompts for consistency
- Extract magic strings as constants
- Mark tests as incomplete in tasks.md
2025-08-12 16:12:15 +10:00
Tabish Bidiwale e9417fc147 Merge pull request #11 from Fission-AI/TabishB/abandon-status-command-change
chore: abandon add-status-command change
2025-08-12 01:03:39 +10:00
Tabish Bidiwale 8bcf2c6905 feat(openspec): add list command change proposal 2025-08-12 00:59:45 +10:00
Tabish Bidiwale 467346f9fe feat: add diff command to view spec changes 2025-08-12 00:59:43 +10:00
Tabish Bidiwale 9a03ba1853 chore: abandon add-status-command change 2025-08-12 00:59:28 +10:00
Tabish Bidiwale 581a681a47 chore: archive add-complexity-guidelines change after deployment 2025-08-11 23:40:25 +10:00
Tabish Bidiwale 3093ca6ae6 Merge pull request #10 from Fission-AI/feat/complexity-guidelines
docs(openspec): add complexity guidelines to README and templates
2025-08-11 23:32:43 +10:00
Tabish Bidiwale 9d425865c1 docs(openspec): add complexity management guidelines and update templates 2025-08-11 23:17:42 +10:00
Tabish Bidiwale a490dbbc78 chore: archive add-update-command change and update specs 2025-08-11 22:29:34 +10:00
Tabish Bidiwale fc0e2319b1 Merge pull request #9 from Fission-AI/add-update-command-impl
feat(cli): add update command
2025-08-11 22:15:28 +10:00
Tabish Bidiwale edf6873afa feat(cli): add update command to refresh OpenSpec instructions and CLAUDE.md via markers 2025-08-11 21:37:46 +10:00
Tabish Bidiwale c0ce4adc0a Merge pull request #8 from Fission-AI/add-update-command
Add update command for OpenSpec instructions
2025-08-11 20:48:04 +10:00
Tabish Bidiwale e40abe1f20 docs(openspec): align add-update-command change with conventions and idempotency 2025-08-09 19:22:55 +10:00
Tabish Bidiwale e752b3200f refactor: simplify update command proposal to remove version tracking 2025-08-07 01:22:34 +10:00
Tabish Bidiwale a59284839b feat: add change proposal for openspec update command 2025-08-07 01:16:03 +10:00
Tabish Bidiwale fa824fac95 chore(openspec): archive completed init command change 2025-08-07 01:04:53 +10:00
Tabish Bidiwale 7bc54b2cd3 Merge pull request #7 from Fission-AI/add-init-command
Add init command for OpenSpec
2025-08-07 00:59:01 +10:00
Tabish Bidiwale a913546ee3 docs: mark test tasks as complete in init command change 2025-08-07 00:57:19 +10:00
Tabish Bidiwale 958fa0aecc feat: add init command and file system utilities 2025-08-07 00:43:04 +10:00
Tabish Bidiwale b358ad781c refactor: move tests to separate test directory 2025-08-07 00:42:31 +10:00
Tabish Bidiwale cc01cca551 docs: update init command spec with implementation details and mark tasks complete 2025-08-07 00:18:08 +10:00
Tabish Bidiwale 9847718af2 feat: integrate init command with CLI and add ora for terminal aesthetics 2025-08-07 00:17:37 +10:00
Tabish Bidiwale 281297695a feat: add core infrastructure for openspec init command 2025-08-07 00:16:59 +10:00
Tabish Bidiwale 5a425edf4d feat: enhance init command with AI tool support and improved UX 2025-08-06 22:24:02 +10:00
Tabish Bidiwale 78751d75ca chore: archive adopt-future-state-storage change 2025-08-06 21:54:21 +10:00
Tabish Bidiwale 8440da50b8 Merge pull request #6 from Fission-AI/add-complexity-guidelines
feat: add complexity management guidelines
2025-08-06 21:41:25 +10:00
Tabish Bidiwale bf9b148000 Merge pull request #5 from Fission-AI/add-status-command
feat: add status command change proposal
2025-08-06 21:40:41 +10:00
Tabish Bidiwale 326cb0febc add complexity management guidelines to prevent over-engineering 2025-08-06 21:23:22 +10:00
Tabish Bidiwale 7ae14d5858 simplify status command proposal to minimal scope 2025-08-06 21:17:53 +10:00
Tabish Bidiwale e08f1a2e60 feat: add change proposal for status command 2025-08-06 18:09:08 +10:00
Tabish Bidiwale d00d66a89d Merge pull request #4 from Fission-AI/adopt-future-state-storage
feat: adopt future state storage for OpenSpec changes
2025-08-06 17:35:05 +10:00
Tabish Bidiwale cb5ac65d03 feat: adopt future state storage for OpenSpec changes 2025-08-06 16:50:00 +10:00
Tabish Bidiwale 4564229a60 Merge pull request #3 from Fission-AI/update-spec-change-format
feat: Update spec change format
2025-08-06 15:52:48 +10:00
Tabish Bidiwale 79c4fa3122 Merge pull request #2 from Fission-AI/add-project-init-change
feat: add change proposal for openspec init command
2025-08-06 15:52:00 +10:00
Tabish Bidiwale 6b8845b10a remove any updates to the proposal format 2025-08-06 15:42:19 +10:00
Tabish Bidiwale b5be00bfe5 Add change for updating spec storage format 2025-08-06 14:26:19 +10:00
54 changed files with 5247 additions and 109 deletions
@@ -0,0 +1,19 @@
# Add Status Command to OpenSpec CLI
## Why
Developers need to know which changes have all tasks completed and are ready to archive.
## What Changes
- Add `openspec status` command that scans the changes/ directory
- Parse each tasks.md file to count `[x]` (complete) and `[ ]` (incomplete) tasks
- Display each change with its completion status (e.g., "auth-feature: 5/5" or "auth-feature: ✓")
- Skip the archive/ subdirectory
## Impact
- Affected specs: New capability `cli-status` will be added
- Affected code:
- `src/cli/index.ts` - Add status command
- `src/core/status.ts` - New file with simple scanning and parsing logic (~50 lines)
@@ -0,0 +1,58 @@
# CLI Status Command Specification
## Purpose
The status command shows which OpenSpec changes are ready to archive by displaying task completion status for each change.
## Command Interface
```bash
# Show status of all changes
openspec status
```
## Behavior
WHEN the status command runs:
1. Scan the `openspec/changes/` directory
2. Skip the `archive/` subdirectory
3. For each change directory with a `tasks.md` file:
- Count tasks marked with `[x]` (case-insensitive)
- Count tasks marked with `[ ]`
- Display the change name and completion status
## Output Format
```
add-auth-feature: 15/15
fix-payment-bug: 8/8
refactor-api: 3/10
update-docs: 0/5
```
Or with checkmark for fully complete:
```
add-auth-feature: ✓
fix-payment-bug: ✓
refactor-api: 3/10
update-docs: 0/5
```
## Task Detection
The command recognizes these patterns as tasks:
- `- [ ]` Incomplete task
- `- [x]` Complete task (lowercase)
- `- [X]` Complete task (uppercase)
## Error Handling
- If no `tasks.md` exists, skip that change
- If `tasks.md` is empty or has no tasks, skip that change
- Continue scanning even if individual files have errors
## Exit Codes
- `0`: Success - status displayed
- `1`: Error - unable to scan changes directory
@@ -0,0 +1,8 @@
# Implementation Tasks for Status Command
## Core Implementation
- [ ] Add status command to `src/cli/index.ts`
- [ ] Create `src/core/status.ts` with directory scanning logic
- [ ] Parse tasks.md files to count `[x]` and `[ ]` patterns
- [ ] Display each change with completion status (name: complete/total)
- [ ] Skip the archive/ subdirectory when scanning
+34 -8
View File
@@ -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,9 +43,9 @@ openspec/
│ │ ├── proposal.md # Why, what, impact (consolidated)
│ │ ├── tasks.md # Implementation checklist
│ │ ├── design.md # Technical decisions (optional, for complex changes)
│ │ └── patches/ # Spec intent changes
│ │ └── specs/ # Future state of affected specs
│ │ └── [capability]/
│ │ └── spec.md.diff
│ │ └── spec.md # Clean markdown (no diff syntax)
│ └── archive/ # Completed changes (dated)
```
@@ -72,6 +89,11 @@ Before any task:
- 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:
@@ -91,12 +113,13 @@ openspec/changes/[descriptive-name]/
- Affected specs: [list capabilities that will change]
- Affected code: [list key files/systems]
# 3. Create patches for ALL spec changes
# - For EXISTING capabilities: show what changes (+ for additions, - for removals)
# - For NEW capabilities: show entire new spec with + prefix on every line
patches/
# 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.diff
└── spec.md
# 4. Create tasks.md with implementation steps
## 1. [Task Group]
@@ -382,10 +405,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
@@ -441,6 +466,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,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
@@ -1,66 +0,0 @@
+# CLI Init Specification
+
+## Purpose
+
+The `openspec init` command SHALL create a complete OpenSpec directory structure in any project, enabling immediate adoption of OpenSpec conventions.
+
+## Behavior
+
+### 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 customizable project context template
+
+### Interactive Mode (Default)
+
+WHEN run without flags
+THEN prompt user for:
+- Project name
+- Project description
+- Technology stack
+- Key conventions
+
+### Non-Interactive Mode
+
+WHEN run with `--yes` flag
+THEN use sensible defaults for all prompts
+
+WHEN run with `--no-input` flag
+THEN skip all prompts and use minimal defaults
+
+### Safety Checks
+
+WHEN `openspec/` directory already exists
+THEN exit with error unless `--force` flag is provided
+
+WHEN `--force` flag is provided
+THEN backup existing directory before overwriting
+
+### Exit Codes
+
+- 0: Success
+- 1: OpenSpec directory already exists
+- 2: Insufficient permissions
+- 3: User cancelled operation
+
+## 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
@@ -1,30 +0,0 @@
# Implementation Tasks for Init Command
## 1. Core Infrastructure
- [ ] 1.1 Create src/utils/file-system.ts with directory/file creation utilities
- [ ] 1.2 Create src/core/templates/index.ts for template management
- [ ] 1.3 Create src/core/init.ts with main initialization logic
## 2. Template Files
- [ ] 2.1 Create src/core/templates/readme-template.ts with OpenSpec README content
- [ ] 2.2 Create src/core/templates/project-template.ts with customizable project.md
- [ ] 2.3 Create src/core/templates/gitignore-template.ts for OpenSpec-specific ignores
## 3. Init Command Implementation
- [ ] 3.1 Add init command to src/cli/index.ts using Commander
- [ ] 3.2 Implement interactive prompts for project information
- [ ] 3.3 Add validation for existing OpenSpec directories
- [ ] 3.4 Implement directory structure creation logic
- [ ] 3.5 Implement file generation with templates
## 4. User Experience
- [ ] 4.1 Add colorful console output for better UX
- [ ] 4.2 Implement progress indicators during creation
- [ ] 4.3 Add success message with next steps
- [ ] 4.4 Add error handling with helpful messages
## 5. Testing and Documentation
- [ ] 5.1 Add unit tests for file system utilities
- [ ] 5.2 Add integration tests for init command
- [ ] 5.3 Update package.json with proper bin configuration
- [ ] 5.4 Test the built CLI command end-to-end
@@ -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
- [ ] 1.1 Create `src/core/list.ts` with list logic
- [ ] 1.1.1 Implement directory scanning (exclude archive/)
- [ ] 1.1.2 Implement task counting from tasks.md files
- [ ] 1.1.3 Format output as simple table
- [ ] 1.2 Add list command to CLI in `src/cli/index.ts`
- [ ] 1.2.1 Register `openspec list` command
- [ ] 1.2.2 Connect to list.ts implementation
## 2. Error Handling
- [ ] 2.1 Handle missing openspec/changes/ directory
- [ ] 2.2 Handle changes without tasks.md files
- [ ] 2.3 Handle empty changes directory
## 3. Testing
- [ ] 3.1 Add tests for list functionality
- [ ] 3.1.1 Test with multiple changes
- [ ] 3.1.2 Test with completed changes
- [ ] 3.1.3 Test with no changes
- [ ] 3.1.4 Test error conditions
## 4. Documentation
- [ ] 4.1 Update CLI help text with list command
- [ ] 4.2 Add list command to README if applicable
@@ -0,0 +1,15 @@
# Add @requirement Markers for Requirement Identification
## Why
Specs contain WHEN/THEN patterns that define system requirements, but extracting these programmatically requires brittle regex parsing that may miss edge cases or break with formatting changes.
## What Changes
- Define @requirement marker convention for identifying key requirements in specs
- Each marker includes a brief identifier (e.g., @requirement user-register)
- Markers appear directly before their WHEN/THEN blocks
- Document convention in openspec-conventions spec
## Impact
- Affected specs: openspec-conventions (new)
- Affected code: None initially - enables future tooling
- Breaking changes: None - additive convention only
@@ -0,0 +1,219 @@
# 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
## Requirement Markers
### Marker Syntax
@requirement marker-syntax
WHEN writing a requirement in a spec
THEN prefix it with @requirement followed by a brief kebab-case identifier
AND place the marker on the line immediately before the WHEN statement
@requirement marker-identifier
WHEN choosing an identifier for @requirement
THEN use kebab-case (lowercase with hyphens)
AND keep it brief but descriptive (2-4 words)
AND ensure it's unique within the spec
@requirement marker-placement
WHEN adding @requirement markers to a spec
THEN place them in the ## Behavior or ## Behaviors section
AND ensure each WHEN/THEN block has exactly one marker
AND maintain a blank line after each THEN block for readability
### Examples
@requirement valid-marker-example
WHEN a spec includes properly formatted markers
THEN tools can extract and identify requirements programmatically
AND the spec remains human-readable
Example of correct usage:
```markdown
## Behavior
@requirement user-register
WHEN user registers with valid email
THEN create account and send confirmation
@requirement user-login
WHEN user logs in with correct credentials
THEN return JWT token with user data
@requirement invalid-credentials
WHEN user provides invalid credentials
THEN return 401 unauthorized error
```
@requirement invalid-marker-detection
WHEN a requirement lacks an @requirement marker
THEN tools should gracefully skip it
AND optionally warn about unmarked requirements
### Edge Cases
@requirement multiline-when-then
WHEN a WHEN or THEN clause spans multiple lines
THEN the @requirement marker still goes on the line before WHEN
AND the entire block is considered part of that requirement
@requirement multiple-then-clauses
WHEN a requirement has multiple THEN clauses using AND
THEN treat them as part of the same requirement
AND use a single @requirement marker for the entire block
@requirement nested-conditions
WHEN requirements have nested conditions or complex logic
THEN keep the @requirement marker simple
AND let the WHEN/THEN content contain the complexity
## Spec Structure
@requirement spec-file-location
WHEN creating a spec file
THEN place it in openspec/specs/[capability-name]/spec.md
AND use kebab-case for the capability name
@requirement spec-sections
WHEN structuring a spec
THEN include these sections in order:
- # [Capability Name] Specification
- ## Purpose (brief description)
- ## Behavior or ## Behaviors (with @requirement markers)
- ## Examples (optional, for complex requirements)
## Benefits of Requirement Markers
@requirement tooling-extraction
WHEN tools need to extract requirements from specs
THEN they can parse @requirement markers reliably
AND avoid complex regex patterns for WHEN/THEN extraction
@requirement requirement-counting
WHEN displaying change summaries
THEN tools can count requirements by counting @requirement markers
AND show accurate requirement counts per spec
@requirement requirement-referencing
WHEN documenting or discussing specific requirements
THEN use the @requirement identifier for clear reference
AND maintain consistency across documentation
## 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,24 @@
# Implementation Tasks
## 1. Define Convention
- [ ] 1.1 Document @requirement marker syntax
- [ ] 1.2 Define identifier naming guidelines
- [ ] 1.3 Specify marker placement rules
- [ ] 1.4 Add examples of proper usage
## 2. Create Specification
- [ ] 2.1 Write openspec-conventions spec
- [ ] 2.2 Include requirement marker section
- [ ] 2.3 Add good and bad examples
- [ ] 2.4 Document edge cases
## 3. Update Existing Specs
- [ ] 3.1 Add @requirement markers to cli-init spec
- [ ] 3.2 Add @requirement markers to cli-update spec
- [ ] 3.3 Add @requirement markers to cli-view spec
- [ ] 3.4 Review and update any other existing specs
## 4. Documentation
- [ ] 4.1 Update README with marker convention
- [ ] 4.2 Add marker usage to CLAUDE.md
- [ ] 4.3 Create examples for AI assistants
@@ -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
@@ -8,9 +8,13 @@ Projects need a simple way to adopt OpenSpec conventions. Currently, users must
- Add `openspec init` CLI command that creates the complete OpenSpec directory structure
- Generate template files (README.md with AI instructions, project.md template)
- Interactive prompts to gather project-specific information
- 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 success/error messages to guide users
- 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
@@ -22,4 +26,5 @@ Projects need a simple way to adopt OpenSpec conventions. Currently, users must
- 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)
@@ -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
@@ -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
+148
View File
@@ -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
+59
View File
@@ -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)
+120
View File
@@ -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
+10 -2
View File
@@ -37,6 +37,9 @@
"build": "node build.js",
"dev": "tsc --watch",
"dev:cli": "pnpm build && node bin/openspec.js",
"test": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest --coverage",
"prepare": "npm run build"
},
"engines": {
@@ -44,10 +47,15 @@
},
"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"
}
}
+1198
View File
File diff suppressed because it is too large Load Diff
+83
View File
@@ -1,4 +1,11 @@
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';
const program = new Command();
@@ -7,4 +14,80 @@ program
.description('AI-native system for spec-driven development')
.version('0.0.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')
.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')
.action(async () => {
try {
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);
}
});
program.parse();
+17
View File
@@ -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 }
];
+6
View File
@@ -0,0 +1,6 @@
export interface ToolConfigurator {
name: string;
configFileName: string;
isAvailable: boolean;
configure(projectPath: string, openspecDir: string): Promise<void>;
}
+23
View File
@@ -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
);
}
}
+28
View File
@@ -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);
}
}
+179
View File
@@ -0,0 +1,179 @@
import { promises as fs } from 'fs';
import path from 'path';
import chalk from 'chalk';
import { diffStringsUnified } from 'jest-diff';
import { select } from '@inquirer/prompts';
// 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;
}
// 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;
}
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;
}
}
+133
View File
@@ -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];
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');
}
}
+90
View File
@@ -0,0 +1,90 @@
import { promises as fs } from 'fs';
import path from 'path';
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 tasksPath = path.join(changesDir, changeDir, 'tasks.md');
let completedTasks = 0;
let incompleteTasks = 0;
try {
const content = await fs.readFile(tasksPath, 'utf-8');
const lines = content.split('\n');
for (const line of lines) {
if (line.includes('- [x]')) {
completedTasks++;
} else if (line.includes('- [ ]')) {
incompleteTasks++;
}
}
} catch {
// No tasks.md file
changes.push({
name: changeDir,
completedTasks: 0,
totalTasks: 0
});
continue;
}
changes.push({
name: changeDir,
completedTasks,
totalTasks: completedTasks + incompleteTasks
});
}
// 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);
let status: string;
if (change.totalTasks === 0) {
status = 'No tasks';
} else if (change.completedTasks === change.totalTasks) {
status = '✓ Complete';
} else {
status = `${change.completedTasks}/${change.totalTasks} tasks`;
}
console.log(`${padding}${paddedName} ${status}`);
}
}
}
+26
View File
@@ -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
`;
+29
View File
@@ -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';
+38
View File
@@ -0,0 +1,38 @@
export interface ProjectContext {
projectName?: string;
description?: string;
techStack?: string[];
conventions?: string;
}
export const projectTemplate = (context: ProjectContext = {}) => `# ${context.projectName || 'Project'} Context
## Overview
${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]
`;
+473
View File
@@ -0,0 +1,473 @@
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/ # 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.
`;
+35
View File
@@ -0,0 +1,35 @@
import path from 'path';
import { FileSystemUtils } from '../utils/file-system.js';
import { TemplateManager } from './templates/index.js';
import { OPENSPEC_DIR_NAME, OPENSPEC_MARKERS } from './config.js';
import { readmeTemplate } from './templates/readme-template.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 CLAUDE.md (marker-based)
const claudePath = path.join(resolvedProjectPath, 'CLAUDE.md');
const claudeContent = TemplateManager.getClaudeTemplate();
await FileSystemUtils.updateFileWithMarkers(
claudePath,
claudeContent,
OPENSPEC_MARKERS.start,
OPENSPEC_MARKERS.end
);
// 4. Success message (ASCII-safe)
console.log('Updated OpenSpec instructions');
}
}
+93
View File
@@ -0,0 +1,93 @@
import { promises as fs } from 'fs';
import path from 'path';
export class FileSystemUtils {
static async createDirectory(dirPath: string): Promise<void> {
await fs.mkdir(dirPath, { recursive: true });
}
static async fileExists(filePath: string): Promise<boolean> {
try {
await fs.access(filePath);
return true;
} catch (error: any) {
if (error.code !== 'ENOENT') {
console.debug(`Unable to check if file exists at ${filePath}: ${error.message}`);
}
return false;
}
}
static async directoryExists(dirPath: string): Promise<boolean> {
try {
const stats = await fs.stat(dirPath);
return stats.isDirectory();
} catch (error: any) {
if (error.code !== 'ENOENT') {
console.debug(`Unable to check if directory exists at ${dirPath}: ${error.message}`);
}
return false;
}
}
static async writeFile(filePath: string, content: string): Promise<void> {
const dir = path.dirname(filePath);
await this.createDirectory(dir);
await fs.writeFile(filePath, content, 'utf-8');
}
static async readFile(filePath: string): Promise<string> {
return await fs.readFile(filePath, 'utf-8');
}
static async updateFileWithMarkers(
filePath: string,
content: string,
startMarker: string,
endMarker: string
): Promise<void> {
let existingContent = '';
if (await this.fileExists(filePath)) {
existingContent = await this.readFile(filePath);
const startIndex = existingContent.indexOf(startMarker);
const endIndex = existingContent.indexOf(endMarker);
if (startIndex !== -1 && endIndex !== -1) {
const before = existingContent.substring(0, startIndex);
const after = existingContent.substring(endIndex + endMarker.length);
existingContent = before + startMarker + '\n' + content + '\n' + endMarker + after;
} else if (startIndex === -1 && endIndex === -1) {
existingContent = startMarker + '\n' + content + '\n' + endMarker + '\n\n' + existingContent;
} else {
throw new Error(`Invalid marker state in ${filePath}. Found start: ${startIndex !== -1}, Found end: ${endIndex !== -1}`);
}
} else {
existingContent = startMarker + '\n' + content + '\n' + endMarker;
}
await this.writeFile(filePath, existingContent);
}
static async ensureWritePermissions(dirPath: string): Promise<boolean> {
try {
// If directory doesn't exist, check parent directory permissions
if (!await this.directoryExists(dirPath)) {
const parentDir = path.dirname(dirPath);
if (!await this.directoryExists(parentDir)) {
await this.createDirectory(parentDir);
}
return await this.ensureWritePermissions(parentDir);
}
const testFile = path.join(dirPath, '.openspec-test-' + Date.now());
await fs.writeFile(testFile, '');
await fs.unlink(testFile);
return true;
} catch (error: any) {
console.debug(`Insufficient permissions to write to ${dirPath}: ${error.message}`);
return false;
}
}
}
+183
View File
@@ -0,0 +1,183 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { promises as fs } from 'fs';
import path from 'path';
import os from 'os';
import { InitCommand } from '../../src/core/init.js';
import * as prompts from '@inquirer/prompts';
vi.mock('@inquirer/prompts', () => ({
select: vi.fn()
}));
describe('InitCommand', () => {
let testDir: string;
let initCommand: InitCommand;
beforeEach(async () => {
testDir = path.join(os.tmpdir(), `openspec-init-test-${Date.now()}`);
await fs.mkdir(testDir, { recursive: true });
initCommand = new InitCommand();
// Mock console.log to suppress output during tests
vi.spyOn(console, 'log').mockImplementation(() => {});
});
afterEach(async () => {
await fs.rm(testDir, { recursive: true, force: true });
vi.restoreAllMocks();
});
describe('execute', () => {
it('should create OpenSpec directory structure', async () => {
vi.mocked(prompts.select).mockResolvedValue('claude');
await initCommand.execute(testDir);
const openspecPath = path.join(testDir, 'openspec');
expect(await directoryExists(openspecPath)).toBe(true);
expect(await directoryExists(path.join(openspecPath, 'specs'))).toBe(true);
expect(await directoryExists(path.join(openspecPath, 'changes'))).toBe(true);
expect(await directoryExists(path.join(openspecPath, 'changes', 'archive'))).toBe(true);
});
it('should create README.md and project.md', async () => {
vi.mocked(prompts.select).mockResolvedValue('claude');
await initCommand.execute(testDir);
const openspecPath = path.join(testDir, 'openspec');
expect(await fileExists(path.join(openspecPath, 'README.md'))).toBe(true);
expect(await fileExists(path.join(openspecPath, 'project.md'))).toBe(true);
const readmeContent = await fs.readFile(path.join(openspecPath, 'README.md'), 'utf-8');
expect(readmeContent).toContain('OpenSpec Instructions');
const projectContent = await fs.readFile(path.join(openspecPath, 'project.md'), 'utf-8');
expect(projectContent).toContain('Project Context');
});
it('should create CLAUDE.md when Claude Code is selected', async () => {
vi.mocked(prompts.select).mockResolvedValue('claude');
await initCommand.execute(testDir);
const claudePath = path.join(testDir, 'CLAUDE.md');
expect(await fileExists(claudePath)).toBe(true);
const content = await fs.readFile(claudePath, 'utf-8');
expect(content).toContain('<!-- OPENSPEC:START -->');
expect(content).toContain('OpenSpec Project');
expect(content).toContain('<!-- OPENSPEC:END -->');
});
it('should update existing CLAUDE.md with markers', async () => {
vi.mocked(prompts.select).mockResolvedValue('claude');
const claudePath = path.join(testDir, 'CLAUDE.md');
const existingContent = '# My Project Instructions\nCustom instructions here';
await fs.writeFile(claudePath, existingContent);
await initCommand.execute(testDir);
const updatedContent = await fs.readFile(claudePath, 'utf-8');
expect(updatedContent).toContain('<!-- OPENSPEC:START -->');
expect(updatedContent).toContain('OpenSpec Project');
expect(updatedContent).toContain('<!-- OPENSPEC:END -->');
expect(updatedContent).toContain('Custom instructions here');
});
it('should throw error if OpenSpec already exists', async () => {
const openspecPath = path.join(testDir, 'openspec');
await fs.mkdir(openspecPath, { recursive: true });
await expect(initCommand.execute(testDir)).rejects.toThrow(
/OpenSpec seems to already be initialized/
);
});
it('should handle non-existent target directory', async () => {
vi.mocked(prompts.select).mockResolvedValue('claude');
const newDir = path.join(testDir, 'new-project');
await initCommand.execute(newDir);
const openspecPath = path.join(newDir, 'openspec');
expect(await directoryExists(openspecPath)).toBe(true);
});
it('should display success message with selected tool name', async () => {
vi.mocked(prompts.select).mockResolvedValue('claude');
const logSpy = vi.spyOn(console, 'log');
await initCommand.execute(testDir);
const calls = logSpy.mock.calls.flat().join('\n');
expect(calls).toContain('Copy these prompts to Claude Code');
});
});
describe('AI tool selection', () => {
it('should prompt for AI tool selection', async () => {
const selectMock = vi.mocked(prompts.select);
selectMock.mockResolvedValue('claude');
await initCommand.execute(testDir);
expect(selectMock).toHaveBeenCalledWith(
expect.objectContaining({
message: 'Which AI tool do you use?'
})
);
});
it('should handle different AI tool selections', async () => {
// For now, only Claude is available, but test the structure
vi.mocked(prompts.select).mockResolvedValue('claude');
await initCommand.execute(testDir);
// When other tools are added, we'd test their specific configurations here
const claudePath = path.join(testDir, 'CLAUDE.md');
expect(await fileExists(claudePath)).toBe(true);
});
});
describe('error handling', () => {
it('should provide helpful error for insufficient permissions', async () => {
// This is tricky to test cross-platform, but we can test the error message
const readOnlyDir = path.join(testDir, 'readonly');
await fs.mkdir(readOnlyDir);
// Mock the permission check to fail
const originalCheck = fs.writeFile;
vi.spyOn(fs, 'writeFile').mockImplementation(async (filePath: any, ...args: any[]) => {
if (typeof filePath === 'string' && filePath.includes('.openspec-test-')) {
throw new Error('EACCES: permission denied');
}
return originalCheck.call(fs, filePath, ...args);
});
await expect(initCommand.execute(readOnlyDir)).rejects.toThrow(
/Insufficient permissions/
);
});
});
});
async function fileExists(filePath: string): Promise<boolean> {
try {
await fs.access(filePath);
return true;
} catch {
return false;
}
}
async function directoryExists(dirPath: string): Promise<boolean> {
try {
const stats = await fs.stat(dirPath);
return stats.isDirectory();
} catch {
return false;
}
}
+165
View File
@@ -0,0 +1,165 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { promises as fs } from 'fs';
import path from 'path';
import os from 'os';
import { ListCommand } from '../../src/core/list.js';
describe('ListCommand', () => {
let tempDir: string;
let originalLog: typeof console.log;
let logOutput: string[] = [];
beforeEach(async () => {
// Create temp directory
tempDir = path.join(os.tmpdir(), `openspec-list-test-${Date.now()}`);
await fs.mkdir(tempDir, { recursive: true });
// Mock console.log to capture output
originalLog = console.log;
console.log = (...args: any[]) => {
logOutput.push(args.join(' '));
};
logOutput = [];
});
afterEach(async () => {
// Restore console.log
console.log = originalLog;
// Clean up temp directory
await fs.rm(tempDir, { recursive: true, force: true });
});
describe('execute', () => {
it('should handle missing openspec/changes directory', async () => {
const listCommand = new ListCommand();
await expect(listCommand.execute(tempDir)).rejects.toThrow(
"No OpenSpec changes directory found. Run 'openspec init' first."
);
});
it('should handle empty changes directory', async () => {
const changesDir = path.join(tempDir, 'openspec', 'changes');
await fs.mkdir(changesDir, { recursive: true });
const listCommand = new ListCommand();
await listCommand.execute(tempDir);
expect(logOutput).toEqual(['No active changes found.']);
});
it('should exclude archive directory', async () => {
const changesDir = path.join(tempDir, 'openspec', 'changes');
await fs.mkdir(path.join(changesDir, 'archive'), { recursive: true });
await fs.mkdir(path.join(changesDir, 'my-change'), { recursive: true });
// Create tasks.md with some tasks
await fs.writeFile(
path.join(changesDir, 'my-change', 'tasks.md'),
'- [x] Task 1\n- [ ] Task 2\n'
);
const listCommand = new ListCommand();
await listCommand.execute(tempDir);
expect(logOutput).toContain('Changes:');
expect(logOutput.some(line => line.includes('my-change'))).toBe(true);
expect(logOutput.some(line => line.includes('archive'))).toBe(false);
});
it('should count tasks correctly', async () => {
const changesDir = path.join(tempDir, 'openspec', 'changes');
await fs.mkdir(path.join(changesDir, 'test-change'), { recursive: true });
await fs.writeFile(
path.join(changesDir, 'test-change', 'tasks.md'),
`# Tasks
- [x] Completed task 1
- [x] Completed task 2
- [ ] Incomplete task 1
- [ ] Incomplete task 2
- [ ] Incomplete task 3
Regular text that should be ignored
`
);
const listCommand = new ListCommand();
await listCommand.execute(tempDir);
expect(logOutput.some(line => line.includes('2/5 tasks'))).toBe(true);
});
it('should show complete status for fully completed changes', async () => {
const changesDir = path.join(tempDir, 'openspec', 'changes');
await fs.mkdir(path.join(changesDir, 'completed-change'), { recursive: true });
await fs.writeFile(
path.join(changesDir, 'completed-change', 'tasks.md'),
'- [x] Task 1\n- [x] Task 2\n- [x] Task 3\n'
);
const listCommand = new ListCommand();
await listCommand.execute(tempDir);
expect(logOutput.some(line => line.includes('✓ Complete'))).toBe(true);
});
it('should handle changes without tasks.md', async () => {
const changesDir = path.join(tempDir, 'openspec', 'changes');
await fs.mkdir(path.join(changesDir, 'no-tasks'), { recursive: true });
const listCommand = new ListCommand();
await listCommand.execute(tempDir);
expect(logOutput.some(line => line.includes('no-tasks') && line.includes('No tasks'))).toBe(true);
});
it('should sort changes alphabetically', async () => {
const changesDir = path.join(tempDir, 'openspec', 'changes');
await fs.mkdir(path.join(changesDir, 'zebra'), { recursive: true });
await fs.mkdir(path.join(changesDir, 'alpha'), { recursive: true });
await fs.mkdir(path.join(changesDir, 'middle'), { recursive: true });
const listCommand = new ListCommand();
await listCommand.execute(tempDir);
const changeLines = logOutput.filter(line =>
line.includes('alpha') || line.includes('middle') || line.includes('zebra')
);
expect(changeLines[0]).toContain('alpha');
expect(changeLines[1]).toContain('middle');
expect(changeLines[2]).toContain('zebra');
});
it('should handle multiple changes with various states', async () => {
const changesDir = path.join(tempDir, 'openspec', 'changes');
// Complete change
await fs.mkdir(path.join(changesDir, 'completed'), { recursive: true });
await fs.writeFile(
path.join(changesDir, 'completed', 'tasks.md'),
'- [x] Task 1\n- [x] Task 2\n'
);
// Partial change
await fs.mkdir(path.join(changesDir, 'partial'), { recursive: true });
await fs.writeFile(
path.join(changesDir, 'partial', 'tasks.md'),
'- [x] Done\n- [ ] Not done\n- [ ] Also not done\n'
);
// No tasks
await fs.mkdir(path.join(changesDir, 'no-tasks'), { recursive: true });
const listCommand = new ListCommand();
await listCommand.execute(tempDir);
expect(logOutput).toContain('Changes:');
expect(logOutput.some(line => line.includes('completed') && line.includes('✓ Complete'))).toBe(true);
expect(logOutput.some(line => line.includes('partial') && line.includes('1/3 tasks'))).toBe(true);
expect(logOutput.some(line => line.includes('no-tasks') && line.includes('No tasks'))).toBe(true);
});
});
});
+162
View File
@@ -0,0 +1,162 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { promises as fs } from 'fs';
import path from 'path';
import os from 'os';
import { FileSystemUtils } from '../../src/utils/file-system.js';
describe('FileSystemUtils', () => {
let testDir: string;
beforeEach(async () => {
testDir = path.join(os.tmpdir(), `openspec-test-${Date.now()}`);
await fs.mkdir(testDir, { recursive: true });
});
afterEach(async () => {
await fs.rm(testDir, { recursive: true, force: true });
});
describe('createDirectory', () => {
it('should create a directory', async () => {
const dirPath = path.join(testDir, 'new-dir');
await FileSystemUtils.createDirectory(dirPath);
const stats = await fs.stat(dirPath);
expect(stats.isDirectory()).toBe(true);
});
it('should create nested directories', async () => {
const dirPath = path.join(testDir, 'nested', 'deep', 'dir');
await FileSystemUtils.createDirectory(dirPath);
const stats = await fs.stat(dirPath);
expect(stats.isDirectory()).toBe(true);
});
it('should not throw if directory already exists', async () => {
const dirPath = path.join(testDir, 'existing-dir');
await fs.mkdir(dirPath);
await expect(FileSystemUtils.createDirectory(dirPath)).resolves.not.toThrow();
});
});
describe('fileExists', () => {
it('should return true for existing file', async () => {
const filePath = path.join(testDir, 'test.txt');
await fs.writeFile(filePath, 'test content');
const exists = await FileSystemUtils.fileExists(filePath);
expect(exists).toBe(true);
});
it('should return false for non-existing file', async () => {
const filePath = path.join(testDir, 'non-existent.txt');
const exists = await FileSystemUtils.fileExists(filePath);
expect(exists).toBe(false);
});
it('should return false for directory path', async () => {
const dirPath = path.join(testDir, 'dir');
await fs.mkdir(dirPath);
const exists = await FileSystemUtils.fileExists(dirPath);
expect(exists).toBe(true); // fs.access doesn't distinguish between files and directories
});
});
describe('directoryExists', () => {
it('should return true for existing directory', async () => {
const dirPath = path.join(testDir, 'test-dir');
await fs.mkdir(dirPath);
const exists = await FileSystemUtils.directoryExists(dirPath);
expect(exists).toBe(true);
});
it('should return false for non-existing directory', async () => {
const dirPath = path.join(testDir, 'non-existent-dir');
const exists = await FileSystemUtils.directoryExists(dirPath);
expect(exists).toBe(false);
});
it('should return false for file path', async () => {
const filePath = path.join(testDir, 'file.txt');
await fs.writeFile(filePath, 'content');
const exists = await FileSystemUtils.directoryExists(filePath);
expect(exists).toBe(false);
});
});
describe('writeFile', () => {
it('should write content to file', async () => {
const filePath = path.join(testDir, 'output.txt');
const content = 'Hello, World!';
await FileSystemUtils.writeFile(filePath, content);
const readContent = await fs.readFile(filePath, 'utf-8');
expect(readContent).toBe(content);
});
it('should create directory if it does not exist', async () => {
const filePath = path.join(testDir, 'nested', 'dir', 'output.txt');
const content = 'Nested content';
await FileSystemUtils.writeFile(filePath, content);
const readContent = await fs.readFile(filePath, 'utf-8');
expect(readContent).toBe(content);
});
it('should overwrite existing file', async () => {
const filePath = path.join(testDir, 'existing.txt');
await fs.writeFile(filePath, 'old content');
const newContent = 'new content';
await FileSystemUtils.writeFile(filePath, newContent);
const readContent = await fs.readFile(filePath, 'utf-8');
expect(readContent).toBe(newContent);
});
});
describe('readFile', () => {
it('should read file content', async () => {
const filePath = path.join(testDir, 'input.txt');
const content = 'Test content';
await fs.writeFile(filePath, content);
const readContent = await FileSystemUtils.readFile(filePath);
expect(readContent).toBe(content);
});
it('should throw for non-existing file', async () => {
const filePath = path.join(testDir, 'non-existent.txt');
await expect(FileSystemUtils.readFile(filePath)).rejects.toThrow();
});
});
describe('ensureWritePermissions', () => {
it('should return true for writable directory', async () => {
const hasPermission = await FileSystemUtils.ensureWritePermissions(testDir);
expect(hasPermission).toBe(true);
});
it('should return true for non-existing directory with writable parent', async () => {
const dirPath = path.join(testDir, 'new-dir');
const hasPermission = await FileSystemUtils.ensureWritePermissions(dirPath);
expect(hasPermission).toBe(true);
});
it('should handle deeply nested non-existing directories', async () => {
const dirPath = path.join(testDir, 'a', 'b', 'c', 'd');
const hasPermission = await FileSystemUtils.ensureWritePermissions(dirPath);
expect(hasPermission).toBe(true);
});
});
});
+252
View File
@@ -0,0 +1,252 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { promises as fs } from 'fs';
import path from 'path';
import os from 'os';
import { FileSystemUtils } from '../../src/utils/file-system.js';
describe('FileSystemUtils.updateFileWithMarkers', () => {
let testDir: string;
const START_MARKER = '<!-- OPENSPEC:START -->';
const END_MARKER = '<!-- OPENSPEC:END -->';
beforeEach(async () => {
testDir = path.join(os.tmpdir(), `openspec-marker-test-${Date.now()}`);
await fs.mkdir(testDir, { recursive: true });
});
afterEach(async () => {
await fs.rm(testDir, { recursive: true, force: true });
});
describe('new file creation', () => {
it('should create new file with markers and content', async () => {
const filePath = path.join(testDir, 'new-file.md');
const content = 'OpenSpec content';
await FileSystemUtils.updateFileWithMarkers(
filePath,
content,
START_MARKER,
END_MARKER
);
const result = await fs.readFile(filePath, 'utf-8');
expect(result).toBe(`${START_MARKER}\n${content}\n${END_MARKER}`);
});
});
describe('existing file without markers', () => {
it('should prepend markers and content to existing file', async () => {
const filePath = path.join(testDir, 'existing.md');
const existingContent = '# Existing Content\nUser content here';
await fs.writeFile(filePath, existingContent);
const newContent = 'OpenSpec content';
await FileSystemUtils.updateFileWithMarkers(
filePath,
newContent,
START_MARKER,
END_MARKER
);
const result = await fs.readFile(filePath, 'utf-8');
expect(result).toBe(
`${START_MARKER}\n${newContent}\n${END_MARKER}\n\n${existingContent}`
);
});
});
describe('existing file with markers', () => {
it('should replace content between markers', async () => {
const filePath = path.join(testDir, 'with-markers.md');
const beforeContent = '# Before\nSome content before';
const oldManagedContent = 'Old OpenSpec content';
const afterContent = '# After\nSome content after';
const existingFile = `${beforeContent}\n${START_MARKER}\n${oldManagedContent}\n${END_MARKER}\n${afterContent}`;
await fs.writeFile(filePath, existingFile);
const newContent = 'New OpenSpec content';
await FileSystemUtils.updateFileWithMarkers(
filePath,
newContent,
START_MARKER,
END_MARKER
);
const result = await fs.readFile(filePath, 'utf-8');
expect(result).toBe(
`${beforeContent}\n${START_MARKER}\n${newContent}\n${END_MARKER}\n${afterContent}`
);
});
it('should preserve content before and after markers', async () => {
const filePath = path.join(testDir, 'preserve.md');
const userContentBefore = '# User Content Before\nImportant user notes';
const userContentAfter = '## User Content After\nMore user notes';
const existingFile = `${userContentBefore}\n${START_MARKER}\nOld content\n${END_MARKER}\n${userContentAfter}`;
await fs.writeFile(filePath, existingFile);
const newContent = 'Updated content';
await FileSystemUtils.updateFileWithMarkers(
filePath,
newContent,
START_MARKER,
END_MARKER
);
const result = await fs.readFile(filePath, 'utf-8');
expect(result).toContain(userContentBefore);
expect(result).toContain(userContentAfter);
expect(result).toContain(newContent);
expect(result).not.toContain('Old content');
});
it('should handle markers at the beginning of file', async () => {
const filePath = path.join(testDir, 'markers-at-start.md');
const afterContent = 'User content after markers';
const existingFile = `${START_MARKER}\nOld content\n${END_MARKER}\n${afterContent}`;
await fs.writeFile(filePath, existingFile);
const newContent = 'New content';
await FileSystemUtils.updateFileWithMarkers(
filePath,
newContent,
START_MARKER,
END_MARKER
);
const result = await fs.readFile(filePath, 'utf-8');
expect(result).toBe(`${START_MARKER}\n${newContent}\n${END_MARKER}\n${afterContent}`);
});
it('should handle markers at the end of file', async () => {
const filePath = path.join(testDir, 'markers-at-end.md');
const beforeContent = 'User content before markers';
const existingFile = `${beforeContent}\n${START_MARKER}\nOld content\n${END_MARKER}`;
await fs.writeFile(filePath, existingFile);
const newContent = 'New content';
await FileSystemUtils.updateFileWithMarkers(
filePath,
newContent,
START_MARKER,
END_MARKER
);
const result = await fs.readFile(filePath, 'utf-8');
expect(result).toBe(`${beforeContent}\n${START_MARKER}\n${newContent}\n${END_MARKER}`);
});
});
describe('invalid marker states', () => {
it('should throw error if only start marker exists', async () => {
const filePath = path.join(testDir, 'invalid-start.md');
const existingFile = `Some content\n${START_MARKER}\nManaged content\nNo end marker`;
await fs.writeFile(filePath, existingFile);
await expect(
FileSystemUtils.updateFileWithMarkers(
filePath,
'New content',
START_MARKER,
END_MARKER
)
).rejects.toThrow(/Invalid marker state/);
});
it('should throw error if only end marker exists', async () => {
const filePath = path.join(testDir, 'invalid-end.md');
const existingFile = `Some content\nNo start marker\nManaged content\n${END_MARKER}`;
await fs.writeFile(filePath, existingFile);
await expect(
FileSystemUtils.updateFileWithMarkers(
filePath,
'New content',
START_MARKER,
END_MARKER
)
).rejects.toThrow(/Invalid marker state/);
});
});
describe('idempotency', () => {
it('should produce same result when called multiple times with same content', async () => {
const filePath = path.join(testDir, 'idempotent.md');
const content = 'Consistent content';
await FileSystemUtils.updateFileWithMarkers(
filePath,
content,
START_MARKER,
END_MARKER
);
const firstResult = await fs.readFile(filePath, 'utf-8');
await FileSystemUtils.updateFileWithMarkers(
filePath,
content,
START_MARKER,
END_MARKER
);
const secondResult = await fs.readFile(filePath, 'utf-8');
expect(secondResult).toBe(firstResult);
});
});
describe('edge cases', () => {
it('should handle empty content', async () => {
const filePath = path.join(testDir, 'empty-content.md');
await FileSystemUtils.updateFileWithMarkers(
filePath,
'',
START_MARKER,
END_MARKER
);
const result = await fs.readFile(filePath, 'utf-8');
expect(result).toBe(`${START_MARKER}\n\n${END_MARKER}`);
});
it('should handle content with special characters', async () => {
const filePath = path.join(testDir, 'special-chars.md');
const content = '# Special chars: ${}[]()<>|\\`*_~';
await FileSystemUtils.updateFileWithMarkers(
filePath,
content,
START_MARKER,
END_MARKER
);
const result = await fs.readFile(filePath, 'utf-8');
expect(result).toContain(content);
});
it('should handle multi-line content', async () => {
const filePath = path.join(testDir, 'multi-line.md');
const content = `Line 1
Line 2
Line 3
Line 5 with gap`;
await FileSystemUtils.updateFileWithMarkers(
filePath,
content,
START_MARKER,
END_MARKER
);
const result = await fs.readFile(filePath, 'utf-8');
expect(result).toContain(content);
});
});
});
+1 -1
View File
@@ -17,5 +17,5 @@
"allowSyntheticDefaultImports": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
"exclude": ["node_modules", "dist", "test"]
}
+22
View File
@@ -0,0 +1,22 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
include: ['test/**/*.test.ts'],
coverage: {
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'dist/',
'bin/',
'*.config.ts',
'build.js',
'test/**'
]
},
testTimeout: 10000,
hookTimeout: 10000
}
});