Compare commits

...
Author SHA1 Message Date
Tabish Bidiwale 4ab65d75dd feat: update conventions to support delta-based changes
- Update openspec-conventions spec with delta-based approach
- Add Header-Based Requirement Identification for programmatic matching
- Define ADDED/MODIFIED/REMOVED/RENAMED sections format
- Document standard output symbols (+ ~ - →)
- Update openspec/README.md with delta conventions and examples
- Update init command template to use delta format
- Mark completed tasks in adopt-delta-based-changes/tasks.md

This implements the first part of the delta-based changes proposal,
updating all documentation and conventions to support the new format.
2025-08-14 18:01:09 +10:00
Tabish Bidiwale 8334006f2b Merge pull request #31 from Fission-AI/adopt-delta-based-changes
feat: adopt delta-based change storage for better reviews
2025-08-14 17:33:50 +10:00
Tabish Bidiwale 8a559e0d00 fix: clarify openspec/README.md in tasks (AI instructions file) 2025-08-14 17:31:37 +10:00
Tabish Bidiwale a3924f17b2 fix: remove specific rendering examples from diff spec 2025-08-14 17:25:12 +10:00
Tabish Bidiwale b11e862b0f chore: reorganize tasks into clearer command-based groups 2025-08-14 17:20:18 +10:00
Tabish Bidiwale 099585afcb fix: remove unnecessary backward compatibility for full-state format 2025-08-14 17:16:18 +10:00
Tabish Bidiwale f023fc317e fix: simplify diff command to show only changes by default 2025-08-14 16:59:47 +10:00
Tabish Bidiwale 38a1463af0 fix: redesign diff command for requirement-level comparison
The diff command now applies deltas and shows side-by-side
requirement comparison rather than just displaying delta instructions.
2025-08-14 16:49:39 +10:00
Tabish Bidiwale f2399d3280 fix: restore implementation tasks that update actual specs
- Added back tasks to update the actual specs (not just proposals)
- Included validation implementation tasks
- Kept implementation-focused structure
- Clarified that specs in changes folder are proposals, not current truth
2025-08-14 12:51:01 +10:00
Tabish Bidiwale f699e10778 fix: remove duplication and simplify spec organization
- CLI specs now reference openspec-conventions for shared concepts
- Added standard output symbols definition to conventions
- Simplified tasks.md to focus on implementation only
- Fixed terminology to consistently use 'normalized header'
2025-08-14 12:38:27 +10:00
Tabish Bidiwale c824d8927f fix: address review feedback for consistency and clarity
- Unify header matching: normalize(header) = trim(header), case-sensitive
- Clarify RENAMED+MODIFIED: MODIFIED must use new header after rename
- Add RENAMED display to cli-diff with → symbol
- Define delta format detection via level-2 heading presence
- Remove RESTRUCTURED marker completely (unnecessary complexity)
- Standardize output symbols: + (added), ~ (modified), - (removed), → (renamed)
2025-08-14 12:14:26 +10:00
Tabish Bidiwale 5821b24ab3 fix: simplify proposal to reduce complexity
- Condense 'What Changes' section to core concepts only
- Simplify Impact section to essentials
- Make Conflict Resolution one concise paragraph
- Remove inline comment from example
- Focus on the key benefit: readable GitHub diffs
2025-08-14 00:02:30 +10:00
Tabish Bidiwale e812eb9e78 fix: remove migration timeline and deprecation notices
- Remove phased migration timeline (project not in use yet)
- Remove deprecation notices from CLI commands
- Keep simple backward compatibility for both formats
2025-08-13 23:59:52 +10:00
Tabish Bidiwale abfe13c5a7 fix: address review feedback on delta-based storage proposal
- Add whitespace normalization for header matching
- Add migration timeline with 3-phase approach over 6 months
- Clarify conflict resolution (handled by Git naturally)
- Replace 'self-contained' with 'complete content' for clarity
2025-08-13 23:57:24 +10:00
Tabish Bidiwale 0d5a75d3a0 feat: add cli-archive and cli-diff spec changes for delta-based storage 2025-08-13 23:49:36 +10:00
Tabish Bidiwale b30c0ad27e chore: remove overly detailed header-matching example 2025-08-13 23:43:14 +10:00
Tabish Bidiwale 1cada18186 feat: propose delta-based change storage for better reviews
- Replace full future state storage with delta-based approach
- Store only ADDED, MODIFIED, RENAMED, and REMOVED requirements
- Use headers as unique identifiers for programmatic matching
- Enable cleaner GitHub reviews showing only actual changes
- Add comprehensive examples and implementation tasks
2025-08-13 23:40:01 +10:00
Tabish Bidiwale fa50b07938 Merge pull request #29 from Fission-AI/fix-update-respects-tool-selection
fix: update command respects existing AI tool files
2025-08-13 23:36:50 +10:00
Tabish Bidiwale b6cad1631c feat: improve error handling and console output clarity
- Added try-catch error handling for configurator failures
- Improved console output to be more specific about what was updated
- Added TODO comment for future multi-configurator test enhancement
- Added test for error handling when configurator fails
- Console now shows 'Updated OpenSpec instructions (README.md)' for clarity
2025-08-13 23:33:23 +10:00
Tabish Bidiwale 2497e81e4d Merge pull request #28 from Fission-AI/add-skip-specs-archive-option
feat: add --skip-specs flag to archive command
2025-08-13 23:31:15 +10:00
Tabish Bidiwale d8cba03840 docs: enhance --skip-specs help text and add implementation notes 2025-08-13 23:27:41 +10:00
Tabish Bidiwale 0b1be19302 fix: update command respects existing AI tool files
The update command now only updates existing AI tool configuration
files instead of forcing CLAUDE.md creation. This allows team members
to use different AI tools without conflicts.

- Modified update.ts to check for existing files before updating
- Added comprehensive tests for the new behavior
- Updated spec and documentation to reflect team-friendly approach
- Created project README documenting the behavior
2025-08-13 23:25:53 +10:00
Tabish Bidiwale d7ebee4555 feat: add --skip-specs flag to archive command and fix confirmation behavior
- Add --skip-specs flag to archive command to skip spec update operations
- Fix confirmation behavior: declining spec updates now continues with archiving instead of cancelling
- Add comprehensive tests for new functionality
- Update task documentation to reflect completed implementation
2025-08-13 23:21:57 +10:00
Tabish Bidiwale 8f45a6f6ee Merge pull request #27 from Fission-AI/fix/update-respects-tool-selection
Fix: Update command respects AI tool selection
2025-08-13 23:11:36 +10:00
Tabish Bidiwale 6da77f01ce Merge pull request #26 from Fission-AI/feat/skip-spec-update-archive-proposal
feat: add skip-specs option for archive command
2025-08-13 23:10:45 +10:00
Tabish Bidiwale d90eccf959 fix: update command respects AI tool selection
The update command now only updates existing AI tool configuration
files instead of forcing CLAUDE.md creation. This allows team members
to use different AI tools without conflicts.
2025-08-13 23:06:05 +10:00
Tabish Bidiwale f192a97aeb feat: add proposal for skip-specs option in archive command
Add change proposal to enable skipping spec updates during archive operation.
This allows archiving changes that don't modify specs (tooling, docs, etc.)
and fixes the confirmation behavior to continue archiving even when users
decline spec updates.
2025-08-13 23:00:16 +10:00
Tabish Bidiwale 7781bbadd3 Merge pull request #25 from Fission-AI/feat/apply-structured-spec-format
feat: apply structured spec format to all specifications
2025-08-13 22:31:43 +10:00
Tabish Bidiwale aeaa1d50cc fix: remove Format Flexibility requirement from conventions
Remove the Format Flexibility section as it's not needed for the structured spec format. Keep focus on behavioral specifications only.
2025-08-13 22:29:10 +10:00
Tabish Bidiwale fa5df9a329 feat: apply structured spec format to all specifications
- Add Specification Format section to openspec-conventions with:
  - Requirement headers for consistent structure
  - Scenario headers with bold WHEN/THEN/AND keywords
  - Format flexibility for different content types

- Update all CLI command specs to use structured format:
  - cli-init: Convert all behavioral sections
  - cli-list: Apply structured format throughout
  - cli-update: Restructure requirements and edge cases
  - cli-diff: Update all behavior sections
  - cli-archive: Convert complex behaviors to scenarios

- Update openspec-conventions spec itself to follow its own format
- Mark all tasks as completed
2025-08-13 22:26:03 +10:00
Tabish Bidiwale f94f396c99 Merge pull request #24 from Fission-AI/cleanup/remove-add-requirement-markers
chore: remove abandoned add-requirement-markers change
2025-08-13 22:10:24 +10:00
Tabish Bidiwale 1f670f71d4 chore: remove abandoned add-requirement-markers change 2025-08-13 22:02:51 +10:00
Tabish Bidiwale 32b2901d13 Merge pull request #23 from Fission-AI/feat/structured-spec-format
feat(openspec): add structured format specification
2025-08-13 21:52:05 +10:00
Tabish Bidiwale 2ad0b1d306 feat: add tasks to update existing specs to new format
Add section 3 with tasks to update all existing CLI command specs to use the new structured format in their Behavior sections
2025-08-13 21:49:26 +10:00
Tabish Bidiwale 279d327899 fix: remove Format Flexibility task for non-behavioral specs
Non-behavioral specs not planned for now, keeping focus on behavioral specifications only
2025-08-13 21:48:36 +10:00
Tabish Bidiwale 5d848cf005 fix: remove unnecessary migration documentation
- Remove migration.md as project has no existing users to migrate
- Remove Migration Support section from tasks.md
- Structured format only applies to behavioral specs, not convention definitions
2025-08-13 21:36:52 +10:00
Tabish Bidiwale 5607fd3ccb fix: remove migration section from spec, keep only in change proposal 2025-08-13 21:21:28 +10:00
Tabish Bidiwale 6a0d862258 refactor: enhance openspec-conventions with structured format
- Merge format rules into openspec-conventions instead of separate spec
- Add Format Flexibility requirement for non-behavioral content
- Address review feedback on gradual migration and alternative formats
2025-08-13 21:17:46 +10:00
Tabish Bidiwale 1fe5f84fbc feat(openspec): add structured format specification for consistency 2025-08-13 21:05:36 +10:00
Tabish Bidiwale 80e78ecd1e chore: archive add-archive-command change after deployment 2025-08-13 18:49:17 +10:00
Tabish Bidiwale 564135a530 archive diff command 2025-08-13 18:47:31 +10:00
Tabish Bidiwale b9e80641a0 Merge pull request #21 from Fission-AI/feat/implement-archive-command
feat: implement archive command for OpenSpec changes
2025-08-13 18:41:54 +10:00
Tabish Bidiwale 0755994eaa refactor: use @inquirer/prompts for consistent UX in archive command
- Replace readline with @inquirer/prompts for all user interactions
- Add arrow key navigation for change selection (consistent with diff command)
- Use confirm() for yes/no prompts with better UX
- Remove manual readline interface management (no longer needed)
- Update tests to mock @inquirer/prompts instead of readline
- Add new test cases for interactive mode behavior

This change provides a consistent user experience across all OpenSpec commands,
where users can navigate options with arrow keys rather than typing numbers.
2025-08-13 18:37:18 +10:00
Tabish Bidiwale dcabd6de31 fix: address PR review feedback for archive command
- Add try-finally block to ensure readline interface always closes
- Extract date formatting to dedicated getArchiveDate() method
- Add comprehensive unit tests for ArchiveCommand covering:
  - Successful archiving flow
  - Incomplete tasks warning
  - Spec updates during archiving
  - Edge cases (missing tasks.md, no specs)
  - Error scenarios (missing change, duplicate archive)
  - No OpenSpec directory error
2025-08-13 18:27:24 +10:00
Tabish Bidiwale aef6ce01ff feat: implement archive command for OpenSpec changes
Adds a new `openspec archive` command that moves completed changes to an archive
directory with date-based naming. The command includes:
- Interactive change selection when no name provided
- Incomplete task warnings before archiving
- Automatic spec updates to main specs directory
- Confirmation prompts (skippable with --yes flag)
- Duplicate archive prevention

Also fixes TypeScript compilation errors in diff.ts and init.ts.
2025-08-13 18:19:26 +10:00
Tabish Bidiwale 441f9f444b Merge pull request #20 from Fission-AI/fix-archive-spec-updates
Fix: Add spec update functionality to archive command
2025-08-13 18:03:09 +10:00
Tabish Bidiwale b322829091 fix: add spec update functionality to archive command
The archive command was missing critical functionality to update main specs
from the change's future state specs when archiving. This fix adds:
- Spec update process that copies future state specs to main specs directory
- Confirmation prompt showing which specs will be created vs updated
- --yes flag for automation scenarios to skip confirmations
- Safety by default with clear visibility into spec changes
2025-08-13 18:00:00 +10:00
Tabish Bidiwale 5167e65a5c chore: archive add-list-command change after deployment 2025-08-13 17:36:12 +10:00
Tabish Bidiwale 5c6b4113a7 Merge pull request #19 from Fission-AI/feat/add-archive-command
feat: add archive command for completed changes
2025-08-13 17:25:34 +10:00
Tabish Bidiwale a8b76c3e69 Merge pull request #18 from Fission-AI/feat/implement-list-command
feat: add list command to show active changes with task status
2025-08-13 17:23:21 +10:00
Tabish Bidiwale d3237cac7b feat: add OpenSpec change proposal for archive command 2025-08-13 17:23:17 +10:00
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 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 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 9a03ba1853 chore: abandon add-status-command change 2025-08-12 00:59:28 +10:00
44 changed files with 3163 additions and 148 deletions
+87
View File
@@ -0,0 +1,87 @@
# OpenSpec
A specification-driven development system for maintaining living documentation alongside your code.
## Installation
```bash
npm install -g openspec
```
## Quick Start
```bash
# Initialize OpenSpec in your project
openspec init
# Update existing OpenSpec instructions (team-friendly)
openspec update
# List all specifications
openspec list
# Show differences between specs and proposed changes
openspec diff [change-name]
# Archive completed changes
openspec archive [change-name]
```
## Commands
### `openspec init`
Initializes OpenSpec in your project by creating:
- `openspec/` directory structure
- `openspec/README.md` with OpenSpec instructions
- AI tool configuration files (based on your selection)
### `openspec update`
Updates OpenSpec instructions to the latest version. This command is **team-friendly** and only updates files that already exist:
- Always updates `openspec/README.md` with the latest OpenSpec instructions
- **Only updates existing AI tool configuration files** (e.g., CLAUDE.md, CURSOR.md)
- **Never creates new AI tool configuration files**
- Preserves content outside of OpenSpec markers in AI tool files
This allows team members to use different AI tools without conflicts. Each developer can maintain their preferred AI tool configuration file, and `openspec update` will respect their choice.
### `openspec list`
Lists all specifications and pending changes in your project:
- Shows current specifications in `openspec/specs/`
- Shows pending changes in `openspec/changes/`
- Shows archived changes in `openspec/changes/archive/`
### `openspec diff [change-name]`
Shows the differences between current specs and proposed changes:
- Displays a unified diff format
- Helps review what will change before implementation
- Useful for pull request reviews
### `openspec archive [change-name]`
Archives a completed change:
- Moves change from `openspec/changes/` to `openspec/changes/archive/`
- Adds a date prefix to the archived change
- Updates specs to reflect the new state
- Use `--skip-specs` to archive without updating specs (for abandoned changes)
## Team Collaboration
OpenSpec is designed for team collaboration:
1. **AI Tool Flexibility**: Each team member can use their preferred AI assistant (Claude, Cursor, etc.)
2. **Non-Invasive Updates**: The `update` command only modifies existing files, never forcing tools on team members
3. **Specification Sharing**: The `openspec/` directory contains shared specifications that all team members work from
4. **Change Tracking**: Proposed changes are visible to all team members for review before implementation
## Contributing
See `openspec/specs/` for the current system specifications and `openspec/changes/` for pending improvements.
## License
MIT
+60 -15
View File
@@ -43,9 +43,9 @@ openspec/
│ │ ├── proposal.md # Why, what, impact (consolidated)
│ │ ├── tasks.md # Implementation checklist
│ │ ├── design.md # Technical decisions (optional, for complex changes)
│ │ └── specs/ # Future state of affected specs
│ │ └── specs/ # Delta changes to specs
│ │ └── [capability]/
│ │ └── spec.md # Clean markdown (no diff syntax)
│ │ └── spec.md # Delta format (ADDED/MODIFIED/REMOVED/RENAMED)
│ └── archive/ # Completed changes (dated)
```
@@ -94,7 +94,35 @@ Before any task:
- 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
### 3. Delta-Based Change Format
Changes use a delta format with clear sections:
```markdown
## ADDED Requirements
### Requirement: New Feature
[Complete requirement content in structured format]
## MODIFIED Requirements
### Requirement: Existing Feature
[Complete modified requirement (header must match current spec)]
## REMOVED Requirements
### Requirement: Old Feature
**Reason for removal**: [Why removing]
**Migration path**: [How to handle existing usage]
## RENAMED Requirements
- FROM: `### Requirement: Old Name`
- TO: `### Requirement: New Name`
```
Key rules:
- Headers are matched using `normalize(header) = trim(header)`
- Include complete requirements (not diffs)
- Use standard symbols in CLI output: + (added), ~ (modified), - (removed), → (renamed)
### 4. Creating a Change Proposal
When a user requests a significant change:
@@ -113,13 +141,21 @@ openspec/changes/[descriptive-name]/
- 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
# 3. Create delta specs for ALL affected capabilities
# - Store only the changes (not complete future state)
# - Use sections: ## ADDED, ## MODIFIED, ## REMOVED, ## RENAMED
# - Include complete requirements in their final form
# Example spec.md content:
# ## ADDED Requirements
# ### Requirement: Password Reset
# Users SHALL be able to reset passwords via email...
#
# ## MODIFIED Requirements
# ### Requirement: User Authentication
# [Complete modified requirement with new password reset hook]
specs/
└── [capability]/
└── spec.md
└── spec.md # Contains delta sections
# 4. Create tasks.md with implementation steps
## 1. [Task Group]
@@ -130,16 +166,16 @@ specs/
[Technical decisions and trade-offs]
```
### 4. The Change Lifecycle
### 5. The Change Lifecycle
1. **Propose** → Create change directory with all documentation
1. **Propose** → Create change directory with delta-based documentation
2. **Review** → User reviews and approves the proposal
3. **Implement** → Follow the approved tasks.md (can be multiple PRs)
4. **Deploy** → User confirms deployment
5. **Update Specs** → Sync specs/ with new reality (IF the change affects system capabilities)
5. **Update Specs** → Apply deltas to sync specs/ with new reality (IF the change affects system capabilities)
6. **Archive** → Move to `changes/archive/YYYY-MM-DD-[name]/`
### 5. Implementing Changes
### 6. Implementing Changes
When implementing an approved change:
1. Follow the tasks.md checklist exactly
@@ -154,7 +190,7 @@ When implementing an approved change:
- 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
### 7. Updating Specs and Archiving After Deployment
**Create a separate PR after deployment** that:
1. Moves change to `changes/archive/YYYY-MM-DD-[name]/`
@@ -163,7 +199,7 @@ When implementing an approved change:
This ensures changes are only archived when truly complete and deployed.
### 7. Types of Changes That Don't Require Specs
### 8. Types of Changes That Don't Require Specs
Some changes only affect development infrastructure and don't need specs:
- Initial project setup (package.json, tsconfig.json, etc.)
@@ -216,7 +252,16 @@ User: "Add password reset functionality"
You should:
1. Read specs/user-auth/spec.md
2. Check changes/ for pending auth changes
3. Create changes/add-password-reset/ with proposal
3. Create changes/add-password-reset/ with:
- proposal.md describing the change
- specs/user-auth/spec.md with:
## ADDED Requirements
### Requirement: Password Reset
[Complete requirement for password reset]
## MODIFIED Requirements
### Requirement: User Authentication
[Updated to integrate with password reset]
4. Wait for approval before implementing
```
@@ -0,0 +1,13 @@
## Why
The archive command currently forces users to either accept spec updates or cancel the entire archive operation. Users need flexibility to archive changes without updating specs, either through explicit flags or by declining the confirmation prompt. This is especially important for changes that don't modify specs (like tooling, documentation, or infrastructure updates).
## What Changes
- Add new `--skip-specs` flag to the archive command that bypasses all spec update operations
- Fix confirmation behavior: when users decline spec updates interactively, proceed with archiving instead of cancelling the entire operation
- When `--skip-specs` flag is used, skip both the spec discovery and update confirmation steps entirely
- Display clear message when specs are skipped (either via flag or user choice)
- Flag can be combined with existing `--yes` flag for fully automated archiving without spec updates
## Impact
- Affected specs: cli-archive
- Affected code: src/core/archive.ts, src/cli/index.ts
@@ -0,0 +1,167 @@
# CLI Archive Command Specification
## Purpose
The archive command moves completed changes from the active changes directory to the archive folder with date-based naming, following OpenSpec conventions.
## Command Syntax
```bash
openspec archive [change-name] [--yes|-y] [--skip-specs]
```
Options:
- `--yes`, `-y`: Skip confirmation prompts (for automation)
- `--skip-specs`: Skip spec update operations entirely (for changes without spec modifications)
## Behavior
### Requirement: Change Selection
The command SHALL support both interactive and direct change selection methods.
#### Scenario: Interactive selection
- **WHEN** no change-name is provided
- **THEN** display interactive list of available changes (excluding archive/)
- **AND** allow user to select one
#### Scenario: Direct selection
- **WHEN** change-name is provided
- **THEN** use that change directly
- **AND** validate it exists
### Requirement: Task Completion Check
The command SHALL verify task completion status before archiving to prevent premature archival.
#### Scenario: Incomplete tasks found
- **WHEN** incomplete tasks are found (marked with `- [ ]`)
- **THEN** display all incomplete tasks to the user
- **AND** prompt for confirmation to continue
- **AND** default to "No" for safety
#### Scenario: All tasks complete
- **WHEN** all tasks are complete OR no tasks.md exists
- **THEN** proceed with archiving without prompting
### Requirement: Archive Process
The archive operation SHALL follow a structured process to safely move changes to the archive.
#### Scenario: Performing archive
- **WHEN** archiving a change
- **THEN** execute these steps:
1. Create archive/ directory if it doesn't exist
2. Generate target name as `YYYY-MM-DD-[change-name]` using current date
3. Check if target directory already exists
4. Update main specs from the change's future state specs unless `--skip-specs` is provided (see Spec Update Process below)
5. Move the entire change directory to the archive location
#### Scenario: Archive already exists
- **WHEN** target archive already exists
- **THEN** fail with error message
- **AND** do not overwrite existing archive
#### Scenario: Successful archive
- **WHEN** move succeeds
- **THEN** display success message with archived name and list of updated specs (if any)
### Requirement: Spec Update Process
Before moving the change to archive, the command SHALL update main specs to reflect the deployed reality unless the `--skip-specs` flag is provided.
#### Scenario: Skipping spec updates
- **WHEN** the `--skip-specs` flag is provided
- **THEN** skip all spec discovery and update operations
- **AND** proceed directly to moving the change to archive
- **AND** display message indicating specs were skipped
#### Scenario: Updating specs from change
- **WHEN** the change contains specs in `changes/[name]/specs/` AND `--skip-specs` is NOT provided
- **THEN** execute these steps:
1. Analyze which specs will be affected by comparing with existing specs
2. Display a summary of spec updates to the user (see Confirmation Behavior below)
3. Prompt for confirmation unless `--yes` flag is provided
4. If confirmed, for each capability spec in the change directory:
- Copy the spec from `changes/[name]/specs/[capability]/spec.md` to `openspec/specs/[capability]/spec.md`
- Create the target directory structure if it doesn't exist
- Overwrite existing spec files (specs represent current reality, change specs are the new reality)
- Track which specs were updated for the success message
#### Scenario: No specs in change
- **WHEN** no specs exist in the change AND `--skip-specs` is NOT provided
- **THEN** skip the spec update step
- **AND** proceed with archiving
### Requirement: Confirmation Behavior
The spec update confirmation SHALL provide clear visibility into changes before they are applied.
#### Scenario: Displaying confirmation
- **WHEN** prompting for confirmation AND `--skip-specs` is NOT provided
- **THEN** display a clear summary showing:
- Which specs will be created (new capabilities)
- Which specs will be updated (existing capabilities)
- The source path for each spec
- **AND** format the confirmation prompt as:
```
The following specs will be updated:
NEW specs to be created:
- cli-archive (from changes/add-archive-command/specs/cli-archive/spec.md)
EXISTING specs to be updated:
- cli-init (from changes/update-init-command/specs/cli-init/spec.md)
Update 2 specs and archive 'add-archive-command'? [y/N]:
```
#### Scenario: Handling confirmation response
- **WHEN** waiting for user confirmation
- **THEN** default to "No" for safety (require explicit "y" or "yes")
- **AND** skip confirmation when `--yes` or `-y` flag is provided
- **AND** skip entire spec confirmation when `--skip-specs` flag is provided
#### Scenario: User declines spec update confirmation
- **WHEN** user declines the spec update confirmation
- **THEN** skip the spec update operations
- **AND** display message: "Skipping spec updates. Proceeding with archive."
- **AND** continue with the archive operation
- **AND** display success message indicating specs were not updated
## Error Handling
### Requirement: Error Conditions
The command SHALL handle various error conditions gracefully.
#### Scenario: Handling errors
- **WHEN** errors occur
- **THEN** handle the following conditions:
- Missing openspec/changes/ directory
- Change not found
- Archive target already exists
- File system permissions issues
## Why These Decisions
**Interactive selection**: Reduces typing and helps users see available changes
**Task checking**: Prevents accidental archiving of incomplete work
**Date prefixing**: Maintains chronological order and prevents naming conflicts
**No overwrite**: Preserves historical archives and prevents data loss
**Spec updates before archiving**: Specs in the main directory represent current reality; when a change is deployed and archived, its future state specs become the new reality and must replace the main specs
**Confirmation for spec updates**: Provides visibility into what will change, prevents accidental overwrites, and ensures users understand the impact before specs are modified
**Non-blocking confirmation**: Declining spec updates doesn't cancel archiving - users can review specs and choose to update them separately if needed
**--yes flag for automation**: Allows CI/CD pipelines to archive without interactive prompts while maintaining safety by default for manual use
**--skip-specs flag**: Enables archiving of changes that don't modify specs (like infrastructure, tooling, or documentation changes) without unnecessary spec update prompts or operations
@@ -0,0 +1,57 @@
## 1. Update Archive Command Implementation
- [x] 1.1 Add `skipSpecs` option to the archive command options interface
- [x] 1.2 Modify the execute method to skip spec operations when flag is set
- [x] 1.3 Fix confirmation behavior: when user declines spec updates, proceed with archiving instead of cancelling
- [x] 1.4 Update console output to indicate when specs are being skipped (via flag or user choice)
- [x] 1.5 Ensure archive continues after declining spec updates
## 2. Update CLI Interface
- [x] 2.1 Add `--skip-specs` flag to the archive command definition
- [x] 2.2 Pass the flag value to the archive command execute method
## 3. Update Tests
- [x] 3.1 Add test case for archiving with --skip-specs flag
- [x] 3.2 Add test case for declining spec updates but continuing with archive
- [x] 3.3 Verify that spec updates are skipped when flag is used
- [x] 3.4 Verify that archive proceeds when user declines spec updates
- [x] 3.5 Ensure existing behavior remains unchanged when flag is not used
## 4. Update Documentation
- [x] 4.1 Update the cli-archive spec to document the new --skip-specs flag
- [x] 4.2 Document the new behavior when declining spec updates interactively
## Implementation Notes
### Key Design Decisions
1. **Non-blocking Confirmation Behavior**: When users decline spec updates interactively, the archive operation continues rather than cancelling entirely. This was a critical UX improvement because:
- Users may want to review specs separately before updating them
- Archiving work shouldn't be blocked by spec review decisions
- Maintains flexibility in the deployment workflow
2. **Flag Naming Convention**: Chose `--skip-specs` for clarity and consistency:
- Clearly indicates the action (skipping) and target (specs)
- Follows kebab-case convention for CLI flags
- Converts naturally to `skipSpecs` camelCase in code
3. **Console Messaging Strategy**: Added explicit messages for all spec-skipping scenarios:
- When flag is used: "Skipping spec updates (--skip-specs flag provided)."
- When user declines: "Skipping spec updates. Proceeding with archive."
- Ensures users always understand what's happening with their specs
4. **Test Coverage Approach**: Created separate test cases for:
- Flag-based skipping (explicit user choice via CLI)
- Interactive declining (runtime user decision)
- Both verify the same outcome but test different code paths
### Use Cases Addressed
- **Infrastructure Changes**: Changes to build tools, CI/CD, dependencies
- **Documentation Updates**: README updates, comment improvements
- **Tooling Modifications**: Developer tools, scripts, configuration files
- **Refactoring**: Code improvements that don't change functionality/specs
### Future Considerations
- Could potentially auto-detect when changes don't include specs and suggest using the flag
- May want to track which archives skipped spec updates for audit purposes
@@ -0,0 +1,66 @@
# Adopt Delta-Based Changes for Specifications
## Why
The current approach of storing complete future states in change proposals creates a poor review experience. When reviewing changes on GitHub, reviewers see entire spec files (often 100+ lines) as "added" in green, making it impossible to identify what actually changed. With the recent structured format adoption, we now have clear section boundaries that enable a better approach: storing only additions and modifications.
## What Changes
Store only the requirements that actually change, not complete future states:
- **ADDED Requirements**: New capabilities being introduced
- **MODIFIED Requirements**: Existing requirements being changed (must match current header)
- **REMOVED Requirements**: Deprecated capabilities
- **RENAMED Requirements**: Explicit header changes (e.g., `FROM: Old Name` → `TO: New Name`)
The archive command will programmatically apply these deltas using normalized header matching (trim leading/trailing whitespace) instead of manually copying entire files.
## Impact
**Affected specs**: openspec-conventions, cli-archive, cli-diff
**Benefits**:
- GitHub diffs show only actual changes (25 lines instead of 150+)
- Reviewers immediately see what's being added, modified, or removed
- Conflicts are more apparent when two changes modify the same requirement
- Archive command can programmatically apply changes
**Format**: Delta format only - all changes must use ADDED/MODIFIED/REMOVED sections.
## Example
Instead of storing a 150-line complete future spec, store only:
```markdown
# User Authentication - Changes
## ADDED Requirements
### Requirement: OAuth Support
Users SHALL authenticate via OAuth providers including Google and GitHub.
#### Scenario: OAuth login flow
- **WHEN** user selects OAuth provider
- **THEN** redirect to provider authorization
- **AND** exchange authorization code for tokens
## MODIFIED Requirements
### Requirement: Session Management
Sessions SHALL expire after 30 minutes of inactivity.
#### Scenario: Inactive session timeout
- **WHEN** no activity for 30 minutes ← (was 60 minutes)
- **THEN** invalidate session token
- **AND** require re-authentication
## RENAMED Requirements
- FROM: `### Requirement: Basic Authentication`
- TO: `### Requirement: Email Authentication`
```
This makes reviews focused and changes explicit.
## Conflict Resolution
Git naturally detects conflicts when two changes modify the same requirement header. This is actually better than full-state storage where Git might silently merge incompatible changes.
@@ -0,0 +1,46 @@
# CLI Archive Command - Changes
## MODIFIED Requirements
### Requirement: Spec Update Process
Before moving the change to archive, the command SHALL apply delta changes to main specs to reflect the deployed reality.
#### Scenario: Applying delta changes
- **WHEN** archiving a change with delta-based specs
- **THEN** parse and apply delta changes as defined in openspec-conventions
- **AND** validate all operations before applying
#### Scenario: Validating delta changes
- **WHEN** processing delta changes
- **THEN** perform validations as specified in openspec-conventions
- **AND** if validation fails, show specific errors and abort
#### Scenario: Conflict detection
- **WHEN** applying deltas would create duplicate requirement headers
- **THEN** abort with error message showing the conflict
- **AND** suggest manual resolution
### Requirement: Display Output
The command SHALL provide clear feedback about delta operations.
#### Scenario: Showing delta application
- **WHEN** applying delta changes
- **THEN** display for each spec:
- Number of requirements added
- Number of requirements modified
- Number of requirements removed
- Number of requirements renamed
- **AND** use standard output symbols (+ ~ - →) as defined in openspec-conventions:
```
Applying changes to specs/user-auth/spec.md:
+ 2 added
~ 3 modified
- 1 removed
→ 1 renamed
```
@@ -0,0 +1,35 @@
# CLI Diff Command - Changes
## REMOVED Requirements
### Requirement: Display Format
**Reason for removal**: The standard unified diff format is replaced by requirement-level side-by-side comparison that better shows semantic changes rather than line-by-line text differences.
## MODIFIED Requirements
### Requirement: Diff Output
The command SHALL show a requirement-level comparison displaying only changed requirements.
#### Scenario: Side-by-side comparison of changes
- **WHEN** running `openspec diff <change>`
- **THEN** display only requirements that have changed
- **AND** show them in a side-by-side format that:
- Clearly shows the current version on the left
- Shows the future version on the right
- Indicates new requirements (not in current)
- Indicates removed requirements (not in future)
- Aligns modified requirements for easy comparison
### Requirement: Validation
The command SHALL validate that changes can be applied successfully.
#### Scenario: Invalid delta references
- **WHEN** delta references non-existent requirement
- **THEN** show error message with specific requirement
- **AND** continue showing other valid changes
- **AND** clearly mark failed changes in the output
@@ -0,0 +1,109 @@
# OpenSpec Conventions - Changes
## ADDED Requirements
### Requirement: Header-Based Requirement Identification
Requirement headers SHALL serve as unique identifiers for programmatic matching between current specs and proposed changes.
#### Scenario: Matching requirements programmatically
- **WHEN** processing delta changes
- **THEN** use the `### Requirement: [Name]` header as the unique identifier
- **AND** match using normalized headers: `normalize(header) = trim(header)`
- **AND** compare headers with case-sensitive equality after normalization
#### Scenario: Handling requirement renames
- **WHEN** renaming a requirement
- **THEN** use a special `## RENAMED Requirements` section
- **AND** specify both old and new names explicitly:
```markdown
## RENAMED Requirements
- FROM: `### Requirement: Old Name`
- TO: `### Requirement: New Name`
```
- **AND** if content also changes, include under MODIFIED using the NEW header
#### Scenario: Validating header uniqueness
- **WHEN** creating or modifying requirements
- **THEN** ensure no duplicate headers exist within a spec
- **AND** validation tools SHALL flag duplicate headers as errors
## MODIFIED Requirements
### Requirement: Change Storage Convention
Change proposals SHALL store only the additions, modifications, and removals to specifications, not complete future states.
#### Scenario: Creating change proposals with additions
- **WHEN** creating a change proposal that adds new requirements
- **THEN** include only the new requirements under `## ADDED Requirements`
- **AND** each requirement SHALL include its complete content
- **AND** use the standard structured format for requirements and scenarios
#### Scenario: Creating change proposals with modifications
- **WHEN** creating a change proposal that modifies existing requirements
- **THEN** include the modified requirements under `## MODIFIED Requirements`
- **AND** use the same header text as in the current spec (normalized)
- **AND** include the complete modified requirement (not a diff)
- **AND** optionally annotate what changed with inline comments like `← (was X)`
#### Scenario: Creating change proposals with removals
- **WHEN** creating a change proposal that removes requirements
- **THEN** list them under `## REMOVED Requirements`
- **AND** use the normalized header text for identification
- **AND** include reason for removal
- **AND** document any migration path if applicable
The `changes/[name]/specs/` directory SHALL contain:
- Delta files showing only what changes
- Sections for ADDED, MODIFIED, REMOVED, and RENAMED requirements
- Normalized header matching for requirement identification
- Complete requirements using the structured format
- Clear indication of change type for each requirement
#### Scenario: Using standard output symbols
- **WHEN** displaying delta operations in CLI output
- **THEN** use these standard symbols:
- `+` for ADDED (green)
- `~` for MODIFIED (yellow)
- `-` for REMOVED (red)
- `→` for RENAMED (cyan)
### Requirement: Archive Process Enhancement
The archive process SHALL programmatically apply delta changes to current specifications using header-based matching.
#### Scenario: Archiving changes with deltas
- **WHEN** archiving a completed change
- **THEN** the archive command SHALL:
1. Parse RENAMED sections first and apply renames
2. Parse REMOVED sections and remove by normalized header match
3. Parse MODIFIED sections and replace by normalized header match (using new names if renamed)
4. Parse ADDED sections and append new requirements
- **AND** validate that all MODIFIED/REMOVED headers exist in current spec
- **AND** validate that ADDED headers don't already exist
- **AND** generate the updated spec in the main specs/ directory
#### Scenario: Handling conflicts during archive
- **WHEN** delta changes conflict with current spec state
- **THEN** the archive command SHALL report specific conflicts
- **AND** require manual resolution before proceeding
- **AND** provide clear guidance on resolving conflicts
## REMOVED Requirements
### Requirement: Future State Storage
**Reason for removal**: Replaced by delta-based change storage which provides better review experience and clearer change tracking.
**Migration path**: All new changes must use delta format.
@@ -0,0 +1,39 @@
# Implementation Tasks
## 1. Update Conventions
- [x] 1.1 Update openspec-conventions spec with delta-based approach
- [x] 1.2 Add Header-Based Requirement Identification
- [x] 1.3 Define ADDED/MODIFIED/REMOVED/RENAMED sections
- [x] 1.4 Document standard output symbols (+ ~ - →)
- [x] 1.5 Update openspec/README.md with delta-based conventions
- [x] 1.6 Update examples to use delta format
## 2. Update Diff Command
- [ ] 2.1 Update cli-diff spec with requirement-level comparison
- [ ] 2.2 Parse specs into requirement-level structures
- [ ] 2.3 Apply deltas to generate future state
- [ ] 2.4 Implement side-by-side comparison view (changes only)
- [ ] 2.5 Add tests for requirement-level comparison
- [ ] 2.6 Add tests for side-by-side view formatting
## 3. Update Archive Command
- [ ] 3.1 Update cli-archive spec with delta processing behavior
- [ ] 3.2 Implement normalized header matching (trim whitespace)
- [ ] 3.3 Parse delta sections (ADDED/MODIFIED/REMOVED/RENAMED)
- [ ] 3.4 Apply changes in order: RENAMED → REMOVED → MODIFIED → ADDED
- [ ] 3.5 Validate delta operations:
- [ ] 3.5.1 MODIFIED/REMOVED requirements exist
- [ ] 3.5.2 ADDED requirements don't already exist
- [ ] 3.5.3 RENAMED FROM headers exist, TO headers don't
- [ ] 3.5.4 No duplicate headers within specs
- [ ] 3.5.5 Renamed requirements aren't also in ADDED
- [ ] 3.6 Display operation counts (+ 2 added, ~ 3 modified, etc.)
- [ ] 3.7 Add tests for header normalization
- [ ] 3.8 Add tests for applying deltas in correct order
- [ ] 3.9 Add tests for validation edge cases
## Notes
- Archive command is critical path - must work reliably
- All new changes must use delta format
- Header normalization: normalize(header) = trim(header)
- Diff command shows only changed requirements in side-by-side comparison
@@ -0,0 +1,20 @@
# Add List Command to OpenSpec CLI
## Why
Developers need visibility into available changes and their status to understand the project's evolution and pending work.
## What Changes
- Add `openspec list` command that displays all changes in the changes/ directory
- Show each change name with task completion count (e.g., "add-auth: 3/5 tasks")
- Display completion status indicator (✓ for fully complete, progress for partial)
- Skip the archive/ subdirectory to focus on active changes
- Simple table output for easy scanning
## Impact
- Affected specs: New capability `cli-list` will be added
- Affected code:
- `src/cli/index.ts` - Add list command
- `src/core/list.ts` - New file with directory scanning and task parsing (~60 lines)
@@ -0,0 +1,69 @@
# List Command Specification
## Purpose
The `openspec list` command SHALL provide developers with a quick overview of all active changes in the project, showing their names and task completion status.
## Behavior
### Command Execution
WHEN `openspec list` is executed
THEN scan the `openspec/changes/` directory for change directories
AND exclude the `archive/` subdirectory from results
AND parse each change's `tasks.md` file to count task completion
### Task Counting
WHEN parsing a `tasks.md` file
THEN count tasks matching these patterns:
- Completed: Lines containing `- [x]`
- Incomplete: Lines containing `- [ ]`
AND calculate total tasks as the sum of completed and incomplete
### Output Format
WHEN displaying the list
THEN show a table with columns:
- Change name (directory name)
- Task progress (e.g., "3/5 tasks" or "✓ Complete")
- Status indicator:
- `✓` for fully completed changes (all tasks done)
- Progress fraction for partial completion
Example output:
```
Changes:
add-auth-feature 3/5 tasks
update-api-docs ✓ Complete
fix-validation 0/2 tasks
add-list-command 1/4 tasks
```
### Empty State
WHEN no active changes exist (only archive/ or empty changes/)
THEN display: "No active changes found."
### Error Handling
IF a change directory has no `tasks.md` file
THEN display the change with "No tasks" status
IF `openspec/changes/` directory doesn't exist
THEN display error: "No OpenSpec changes directory found. Run 'openspec init' first."
AND exit with code 1
### Sorting
Changes SHALL be displayed in alphabetical order by change name for consistency.
## Why
Developers need a quick way to:
- See what changes are in progress
- Identify which changes are ready to archive
- Understand the overall project evolution status
- Get a bird's-eye view without opening multiple files
This command provides that visibility with minimal effort, following OpenSpec's philosophy of simplicity and clarity.
@@ -0,0 +1,26 @@
# Implementation Tasks
## 1. Core Implementation
- [x] 1.1 Create `src/core/list.ts` with list logic
- [x] 1.1.1 Implement directory scanning (exclude archive/)
- [x] 1.1.2 Implement task counting from tasks.md files
- [x] 1.1.3 Format output as simple table
- [x] 1.2 Add list command to CLI in `src/cli/index.ts`
- [x] 1.2.1 Register `openspec list` command
- [x] 1.2.2 Connect to list.ts implementation
## 2. Error Handling
- [x] 2.1 Handle missing openspec/changes/ directory
- [x] 2.2 Handle changes without tasks.md files
- [x] 2.3 Handle empty changes directory
## 3. Testing
- [x] 3.1 Add tests for list functionality
- [x] 3.1.1 Test with multiple changes
- [x] 3.1.2 Test with completed changes
- [x] 3.1.3 Test with no changes
- [x] 3.1.4 Test error conditions
## 4. Documentation
- [x] 4.1 Update CLI help text with list command
- [x] 4.2 Add list command to README if applicable
@@ -0,0 +1,15 @@
## Why
Need a command to archive completed changes to the archive folder with proper date prefixing, following OpenSpec conventions. Currently changes must be manually moved and renamed.
## What Changes
- Add new `archive` command to CLI that moves changes to `changes/archive/YYYY-MM-DD-[change-name]/`
- Check for incomplete tasks before archiving and warn user
- Allow interactive selection of change to archive
- Prevent archiving if target directory already exists
- Update main specs from the change's future state specs (copy from `changes/[name]/specs/` to `openspec/specs/`)
- Show confirmation prompt before updating specs, displaying which specs will be created/updated
- Support `--yes` flag to skip confirmations for automation
## Impact
- Affected specs: cli-archive (new)
- Affected code: src/cli/index.ts, src/core/archive.ts (new)
@@ -0,0 +1,111 @@
# CLI Archive Command Specification
## Purpose
The archive command moves completed changes from the active changes directory to the archive folder with date-based naming, following OpenSpec conventions.
## Command Syntax
```bash
openspec archive [change-name] [--yes|-y]
```
Options:
- `--yes`, `-y`: Skip confirmation prompts (for automation)
## Behavior
### Change Selection
WHEN no change-name is provided
THEN display interactive list of available changes (excluding archive/)
AND allow user to select one
WHEN change-name is provided
THEN use that change directly
AND validate it exists
### Task Completion Check
The command SHALL scan the change's tasks.md file for incomplete tasks (marked with `- [ ]`)
WHEN incomplete tasks are found
THEN display all incomplete tasks to the user
AND prompt for confirmation to continue
AND default to "No" for safety
WHEN all tasks are complete OR no tasks.md exists
THEN proceed with archiving without prompting
### Archive Process
The archive operation SHALL:
1. Create archive/ directory if it doesn't exist
2. Generate target name as `YYYY-MM-DD-[change-name]` using current date
3. Check if target directory already exists
4. Update main specs from the change's future state specs (see Spec Update Process below)
5. Move the entire change directory to the archive location
WHEN target archive already exists
THEN fail with error message
AND do not overwrite existing archive
WHEN move succeeds
THEN display success message with archived name and list of updated specs
### Spec Update Process
Before moving the change to archive, the command SHALL update main specs to reflect the deployed reality:
WHEN the change contains specs in `changes/[name]/specs/`
THEN:
1. Analyze which specs will be affected by comparing with existing specs
2. Display a summary of spec updates to the user (see Confirmation Behavior below)
3. Prompt for confirmation unless `--yes` flag is provided
4. If confirmed, for each capability spec in the change directory:
- Copy the spec from `changes/[name]/specs/[capability]/spec.md` to `openspec/specs/[capability]/spec.md`
- Create the target directory structure if it doesn't exist
- Overwrite existing spec files (specs represent current reality, change specs are the new reality)
- Track which specs were updated for the success message
WHEN no specs exist in the change
THEN skip the spec update step
AND proceed with archiving
### Confirmation Behavior
The spec update confirmation SHALL:
- Display a clear summary showing:
- Which specs will be created (new capabilities)
- Which specs will be updated (existing capabilities)
- The source path for each spec
- Format the confirmation prompt as:
```
The following specs will be updated:
NEW specs to be created:
- cli-archive (from changes/add-archive-command/specs/cli-archive/spec.md)
EXISTING specs to be updated:
- cli-init (from changes/update-init-command/specs/cli-init/spec.md)
Update 2 specs and archive 'add-archive-command'? [y/N]:
```
- Default to "No" for safety (require explicit "y" or "yes")
- Skip confirmation when `--yes` or `-y` flag is provided
WHEN user declines the confirmation
THEN abort the entire archive operation
AND display message: "Archive cancelled. No changes were made."
AND exit with non-zero status code
## Error Handling
SHALL handle the following error conditions:
- Missing openspec/changes/ directory
- Change not found
- Archive target already exists
- File system permissions issues
## Why These Decisions
**Interactive selection**: Reduces typing and helps users see available changes
**Task checking**: Prevents accidental archiving of incomplete work
**Date prefixing**: Maintains chronological order and prevents naming conflicts
**No overwrite**: Preserves historical archives and prevents data loss
**Spec updates before archiving**: Specs in the main directory represent current reality; when a change is deployed and archived, its future state specs become the new reality and must replace the main specs
**Confirmation for spec updates**: Provides visibility into what will change, prevents accidental overwrites, and ensures users understand the impact before specs are modified
**--yes flag for automation**: Allows CI/CD pipelines to archive without interactive prompts while maintaining safety by default for manual use
@@ -0,0 +1,44 @@
# Implementation Tasks
## 1. Core Implementation
- [ ] 1.1 Create `src/core/archive.ts` with ArchiveCommand class
- [ ] 1.1.1 Implement change selection (interactive if not provided)
- [ ] 1.1.2 Implement incomplete task checking from tasks.md
- [ ] 1.1.3 Implement confirmation prompt for incomplete tasks
- [ ] 1.1.4 Implement spec update functionality
- [ ] 1.1.4.1 Detect specs in change directory
- [ ] 1.1.4.2 Compare with existing main specs
- [ ] 1.1.4.3 Display summary of new vs updated specs
- [ ] 1.1.4.4 Show confirmation prompt for spec updates
- [ ] 1.1.4.5 Copy specs to main spec directory
- [ ] 1.1.5 Implement archive move with date prefixing
- [ ] 1.1.6 Support --yes flag to skip confirmations
## 2. CLI Integration
- [ ] 2.1 Add archive command to `src/cli/index.ts`
- [ ] 2.1.1 Import ArchiveCommand
- [ ] 2.1.2 Register command with commander
- [ ] 2.1.3 Add --yes/-y flag option
- [ ] 2.1.4 Add proper error handling
## 3. Error Handling
- [ ] 3.1 Handle missing openspec/changes/ directory
- [ ] 3.2 Handle change not found
- [ ] 3.3 Handle archive target already exists
- [ ] 3.4 Handle user cancellation
## 4. Testing
- [ ] 4.1 Test with fully completed change
- [ ] 4.2 Test with incomplete tasks (warning shown)
- [ ] 4.3 Test interactive selection mode
- [ ] 4.4 Test duplicate archive prevention
- [ ] 4.5 Test spec update functionality
- [ ] 4.5.1 Test creating new specs
- [ ] 4.5.2 Test updating existing specs
- [ ] 4.5.3 Test confirmation prompt display
- [ ] 4.5.4 Test declining confirmation (no changes made)
- [ ] 4.5.5 Test --yes flag skips confirmation
## 5. Build and Validation
- [ ] 5.1 Ensure TypeScript compilation succeeds
- [ ] 5.2 Test command execution
@@ -0,0 +1,28 @@
# Fix Update Command Tool Selection
## Problem
The `openspec update` command currently forces the creation/update of CLAUDE.md regardless of which AI tool was selected during initialization. This violates the tool-agnostic design principle and creates confusion for users who selected different AI assistants.
Additionally, different team members may use different AI tools, so we cannot rely on a shared configuration file.
## Solution
Modify the update command to:
1. Only update AI tool configuration files that already exist
2. Never create new AI tool configuration files
3. Always update the core OpenSpec files (README.md, etc.)
## Implementation
- Remove hardcoded CLAUDE.md update from update command
- Implement file existence check before updating any AI tool config
- Update each existing AI tool config file with its appropriate markers
- No configuration file needed (avoids team conflicts)
## Success Criteria
- Update command only modifies existing AI tool configuration files
- No new AI tool files created during update
- Team members can use different AI tools without conflicts
- Existing projects continue to work (backward compatibility)
@@ -0,0 +1,113 @@
# Update Command Specification
## Purpose
As a developer using OpenSpec, I want to update the OpenSpec instructions in my project when new versions are released, so that I can benefit from improvements to AI agent instructions.
## Core Requirements
### Requirement: Update Behavior
The update command SHALL update OpenSpec instruction files to the latest templates.
#### Scenario: Running update command
- **WHEN** a user runs `openspec update`
- **THEN** the command SHALL:
- Check if the `openspec` directory exists
- Replace `openspec/README.md` with the latest template (complete replacement)
- For each supported AI tool configuration file:
- Check if the file exists (e.g., CLAUDE.md, COPILOT.md)
- If it exists, update it using appropriate markers
- If it doesn't exist, skip it (do NOT create)
- Preserve user content outside markers
- Display ASCII-safe success message: "Updated OpenSpec instructions"
### Requirement: Prerequisites
The command SHALL require an existing OpenSpec structure before allowing updates.
#### Scenario: Checking prerequisites
- **GIVEN** the command requires an existing `openspec` directory (created by `openspec init`)
- **WHEN** the `openspec` directory does not exist
- **THEN** display error: "No OpenSpec directory found. Run 'openspec init' first."
- **AND** exit with code 1
### Requirement: File Handling
The update command SHALL handle file updates in a predictable and safe manner.
#### Scenario: Updating files
- **WHEN** updating files
- **THEN** completely replace `openspec/README.md` with the latest template
- **AND** update only the AI tool configuration files that already exist
- **AND** use the default directory name `openspec`
- **AND** be idempotent (repeated runs have no additional effect)
### Requirement: Tool-Agnostic Updates
The update command SHALL work for any team member regardless of their AI tool choice.
#### Scenario: Team member using Claude
- **GIVEN** a team member has CLAUDE.md in their project
- **WHEN** running `openspec update`
- **THEN** update the CLAUDE.md file with the latest template
- **AND** preserve user content outside OpenSpec markers
- **AND** NOT create files for other tools
#### Scenario: Team member using different tool
- **GIVEN** a team member has COPILOT.md but no CLAUDE.md
- **WHEN** running `openspec update`
- **THEN** update the COPILOT.md file if implementation exists
- **AND** NOT create CLAUDE.md
- **AND** preserve user content outside OpenSpec markers
#### Scenario: Mixed team environment
- **GIVEN** a repository with both CLAUDE.md and COPILOT.md (different team members)
- **WHEN** any team member runs `openspec update`
- **THEN** update all existing AI tool configuration files
- **AND** NOT create new AI tool configuration files
- **AND** each team member's preferred tool remains configured
## Edge Cases
### Requirement: Error Handling
The command SHALL handle edge cases gracefully.
#### Scenario: File permission errors
- **WHEN** file write fails
- **THEN** let the error bubble up naturally with file path
#### Scenario: No AI tool files exist
- **GIVEN** no AI tool configuration files exist
- **WHEN** running update
- **THEN** only update openspec/README.md
- **AND** display success message
#### Scenario: Custom directory names
- **WHEN** considering custom directory names
- **THEN** not supported in this change
- **AND** the default directory name `openspec` SHALL be used
## Success Criteria
Users SHALL be able to:
- Update OpenSpec instructions with a single command
- Get the latest AI agent instructions for their existing tools
- Work in teams where members use different AI tools
- NOT have unwanted AI tool configuration files created
The update process SHALL be:
- Simple and fast (no version checking)
- Predictable (same result every time)
- Self-contained (no network required)
- Team-friendly (respects individual tool choices)
@@ -0,0 +1,21 @@
# Implementation Tasks
## 1. Update Update Command
- [x] Remove hardcoded CLAUDE.md update from `src/core/update.ts`
- [x] Add logic to check for existing AI tool configuration files
- [x] Update only existing files using their appropriate configurators
- [x] Iterate through all registered configurators to check for existing files
## 2. Update Configurator Registry
- [x] Add method to get all configurators for update command
- [x] Ensure each configurator can check if its file exists
## 3. Add Tests
- [x] Test update command with only CLAUDE.md present
- [x] Test update command with no AI tool files present
- [x] Test update command with multiple AI tool files present
- [x] Test that update never creates new AI tool files
## 4. Update Documentation
- [x] Update README to clarify team-friendly behavior
- [x] Document that update only modifies existing files
@@ -0,0 +1,36 @@
## Why
OpenSpec specifications lack a consistent structure that makes sections visually identifiable and programmatically parseable across different specs. This makes it harder to maintain consistency and build tooling.
## What Changes
**Specification Format Section**
- From: No formal structure requirements for specifications
- To: Structured format with `### Requirement:` and `#### Scenario:` headers
- Reason: Visual consistency and parseability across all specs
- Impact: Non-breaking - existing specs can migrate gradually
**Keyword Formatting**
- From: Inconsistent use of WHEN/THEN/AND keywords
- To: Bold keywords (**WHEN**, **THEN**, **AND**) in scenario bullets
- Reason: Improved readability and consistent visual hierarchy
- Impact: Non-breaking - formatting enhancement only
**Format Flexibility**
- From: Implicit understanding that different content needs different formats
- To: Explicit allowance for alternative formats (OpenAPI, JSON Schema, etc.)
- Reason: Address concern that not all specs fit requirement/scenario pattern
- Impact: Non-breaking - clarifies existing practice
**Migration Guidelines**
- From: No migration guidance
- To: Documented gradual migration approach
- Reason: Allows incremental adoption without disrupting existing specs
- Impact: Non-breaking - opt-in migration as specs are modified
## Impact
- Affected specs: openspec-conventions (enhancement to existing capability)
- Affected code: None initially - this is a documentation standard enhancement
- Migration: Gradual - existing specs migrate as they're modified
- Tooling: Enables future parsing tools but doesn't require them
@@ -0,0 +1,180 @@
# 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]/
```
## Specification Format
### Requirement: Structured Format for Behavioral Specs
Behavioral specifications SHALL use a structured format with consistent section headers and keywords to ensure visual consistency and parseability.
#### Scenario: Writing requirement sections
- **WHEN** documenting a requirement in a behavioral specification
- **THEN** use a level-3 heading with format `### Requirement: [Name]`
- **AND** immediately follow with a SHALL statement describing core behavior
- **AND** keep requirement names descriptive and under 50 characters
#### Scenario: Documenting scenarios
- **WHEN** documenting specific behaviors or use cases
- **THEN** use level-4 headings with format `#### Scenario: [Description]`
- **AND** use bullet points with bold keywords for steps:
- **GIVEN** for initial state (optional)
- **WHEN** for conditions or triggers
- **THEN** for expected outcomes
- **AND** for additional outcomes or conditions
#### Scenario: Adding implementation details
- **WHEN** a step requires additional detail
- **THEN** use sub-bullets under the main step
- **AND** maintain consistent indentation
- Sub-bullets provide examples or specifics
- Keep sub-bullets concise
### Requirement: Format Flexibility
The structured format SHALL be the default for behavioral specifications, but alternative formats MAY be used when more appropriate for the content type.
#### Scenario: Documenting API specifications
- **WHEN** documenting REST API endpoints or GraphQL schemas
- **THEN** OpenAPI, GraphQL SDL, or similar formats MAY be used
- **AND** the spec SHALL clearly indicate the format being used
- **AND** behavioral aspects SHALL still follow the structured format
#### Scenario: Documenting data schemas
- **WHEN** documenting data structures, database schemas, or configurations
- **THEN** JSON Schema, SQL DDL, or similar formats MAY be used
- **AND** include the structured format for behavioral rules and constraints
#### Scenario: Using simplified format
- **WHEN** documenting simple capabilities without complex scenarios
- **THEN** a simplified WHEN/THEN format without full structure MAY be used
- **AND** this should be consistent within the capability
## Change Storage Convention
### Future State Storage
WHEN creating a change proposal
THEN store the complete future state of affected specs
AND use clean markdown without diff syntax
The `changes/[name]/specs/` directory SHALL contain:
- Complete spec files as they will exist after the change
- Clean markdown without `+` or `-` prefixes
- All formatting and structure of the final intended state
### Proposal Format
WHEN documenting what changes
THEN the proposal SHALL explicitly describe each change:
```markdown
**[Section or Behavior Name]**
- From: [current state/requirement]
- To: [future state/requirement]
- Reason: [why this change is needed]
- Impact: [breaking/non-breaking, who's affected]
```
This explicit format compensates for not having inline diffs and ensures reviewers understand exactly what will change.
## Change Lifecycle
The change process SHALL follow these states:
1. **Propose**: AI creates change with future state specs and explicit proposal
2. **Review**: Humans review proposal and future state
3. **Approve**: Change is approved for implementation
4. **Implement**: Follow tasks.md checklist (can span multiple PRs)
5. **Deploy**: Changes are deployed to production
6. **Update**: Specs in `specs/` are updated to match deployed reality
7. **Archive**: Change is moved to `archive/YYYY-MM-DD-[name]/`
## Viewing Changes
WHEN reviewing proposed changes
THEN reviewers can compare using:
- GitHub PR diff view when changes are committed
- Command line: `diff -u specs/[capability]/spec.md changes/[name]/specs/[capability]/spec.md`
- Any visual diff tool comparing current vs future state
The system relies on tools to generate diffs rather than storing them.
## Capability Naming
Capabilities SHALL use:
- Verb-noun patterns (e.g., `user-auth`, `payment-capture`)
- Hyphenated lowercase names
- Singular focus (one responsibility per capability)
- No nesting (flat structure under `specs/`)
## When Changes Require Proposals
A proposal SHALL be created for:
- New features or capabilities
- Breaking changes to existing behavior
- Architecture or pattern changes
- Performance optimizations that change behavior
- Security updates affecting access patterns
A proposal is NOT required for:
- Bug fixes restoring intended behavior
- Typos or formatting fixes
- Non-breaking dependency updates
- Adding tests for existing behavior
- Documentation clarifications
## Why This Approach
Clean future state storage provides:
- **Readability**: No diff syntax pollution
- **AI-compatibility**: Standard markdown that AI tools understand
- **Simplicity**: No special parsing or processing needed
- **Tool-agnostic**: Any diff tool can show changes
- **Clear intent**: Explicit proposals document reasoning
The structured format adds:
- **Visual Consistency**: Requirement and Scenario prefixes make sections instantly recognizable
- **Parseability**: Consistent structure enables tooling and automation
- **Flexibility**: Alternative formats supported where appropriate
- **Gradual Adoption**: Existing specs can migrate incrementally
@@ -0,0 +1,19 @@
## 1. Update OpenSpec Conventions Spec
- [x] 1.1 Add "Specification Format" section to openspec-conventions
- [x] 1.2 Document structured format with Requirement/Scenario headers
- [x] 1.3 Define bold keyword usage (WHEN/THEN/AND) for scenarios
- [x] 1.4 Include examples demonstrating the format within the spec itself
## 2. Update Documentation
- [x] 2.1 Update the "Why This Approach" section with structured format benefits
- [x] 2.2 Ensure spec follows its own format as a demonstration
## 3. Update Existing Specs
- [x] 3.1 Update cli-init spec to use structured format in Behavior section
- [x] 3.2 Update cli-list spec to use structured format in Behavior section
- [x] 3.3 Update cli-update spec to use structured format in Behavior section
- [x] 3.4 Update cli-diff spec to use structured format in Behavior section
- [x] 3.5 Update cli-archive spec to use structured format in Behavior section
+155
View File
@@ -0,0 +1,155 @@
# CLI Archive Command Specification
## Purpose
The archive command moves completed changes from the active changes directory to the archive folder with date-based naming, following OpenSpec conventions.
## Command Syntax
```bash
openspec archive [change-name] [--yes|-y]
```
Options:
- `--yes`, `-y`: Skip confirmation prompts (for automation)
## Behavior
### Requirement: Change Selection
The command SHALL support both interactive and direct change selection methods.
#### Scenario: Interactive selection
- **WHEN** no change-name is provided
- **THEN** display interactive list of available changes (excluding archive/)
- **AND** allow user to select one
#### Scenario: Direct selection
- **WHEN** change-name is provided
- **THEN** use that change directly
- **AND** validate it exists
### Requirement: Task Completion Check
The command SHALL verify task completion status before archiving to prevent premature archival.
#### Scenario: Incomplete tasks found
- **WHEN** incomplete tasks are found (marked with `- [ ]`)
- **THEN** display all incomplete tasks to the user
- **AND** prompt for confirmation to continue
- **AND** default to "No" for safety
#### Scenario: All tasks complete
- **WHEN** all tasks are complete OR no tasks.md exists
- **THEN** proceed with archiving without prompting
### Requirement: Archive Process
The archive operation SHALL follow a structured process to safely move changes to the archive.
#### Scenario: Performing archive
- **WHEN** archiving a change
- **THEN** execute these steps:
1. Create archive/ directory if it doesn't exist
2. Generate target name as `YYYY-MM-DD-[change-name]` using current date
3. Check if target directory already exists
4. Update main specs from the change's future state specs (see Spec Update Process below)
5. Move the entire change directory to the archive location
#### Scenario: Archive already exists
- **WHEN** target archive already exists
- **THEN** fail with error message
- **AND** do not overwrite existing archive
#### Scenario: Successful archive
- **WHEN** move succeeds
- **THEN** display success message with archived name and list of updated specs
### Requirement: Spec Update Process
Before moving the change to archive, the command SHALL update main specs to reflect the deployed reality.
#### Scenario: Updating specs from change
- **WHEN** the change contains specs in `changes/[name]/specs/`
- **THEN** execute these steps:
1. Analyze which specs will be affected by comparing with existing specs
2. Display a summary of spec updates to the user (see Confirmation Behavior below)
3. Prompt for confirmation unless `--yes` flag is provided
4. If confirmed, for each capability spec in the change directory:
- Copy the spec from `changes/[name]/specs/[capability]/spec.md` to `openspec/specs/[capability]/spec.md`
- Create the target directory structure if it doesn't exist
- Overwrite existing spec files (specs represent current reality, change specs are the new reality)
- Track which specs were updated for the success message
#### Scenario: No specs in change
- **WHEN** no specs exist in the change
- **THEN** skip the spec update step
- **AND** proceed with archiving
### Requirement: Confirmation Behavior
The spec update confirmation SHALL provide clear visibility into changes before they are applied.
#### Scenario: Displaying confirmation
- **WHEN** prompting for confirmation
- **THEN** display a clear summary showing:
- Which specs will be created (new capabilities)
- Which specs will be updated (existing capabilities)
- The source path for each spec
- **AND** format the confirmation prompt as:
```
The following specs will be updated:
NEW specs to be created:
- cli-archive (from changes/add-archive-command/specs/cli-archive/spec.md)
EXISTING specs to be updated:
- cli-init (from changes/update-init-command/specs/cli-init/spec.md)
Update 2 specs and archive 'add-archive-command'? [y/N]:
```
#### Scenario: Handling confirmation response
- **WHEN** waiting for user confirmation
- **THEN** default to "No" for safety (require explicit "y" or "yes")
- **AND** skip confirmation when `--yes` or `-y` flag is provided
#### Scenario: User declines confirmation
- **WHEN** user declines the confirmation
- **THEN** abort the entire archive operation
- **AND** display message: "Archive cancelled. No changes were made."
- **AND** exit with non-zero status code
## Error Handling
### Requirement: Error Conditions
The command SHALL handle various error conditions gracefully.
#### Scenario: Handling errors
- **WHEN** errors occur
- **THEN** handle the following conditions:
- Missing openspec/changes/ directory
- Change not found
- Archive target already exists
- File system permissions issues
## Why These Decisions
**Interactive selection**: Reduces typing and helps users see available changes
**Task checking**: Prevents accidental archiving of incomplete work
**Date prefixing**: Maintains chronological order and prevents naming conflicts
**No overwrite**: Preserves historical archives and prevents data loss
**Spec updates before archiving**: Specs in the main directory represent current reality; when a change is deployed and archived, its future state specs become the new reality and must replace the main specs
**Confirmation for spec updates**: Provides visibility into what will change, prevents accidental overwrites, and ensures users understand the impact before specs are modified
**--yes flag for automation**: Allows CI/CD pipelines to archive without interactive prompts while maintaining safety by default for manual use
+120
View File
@@ -0,0 +1,120 @@
# CLI Diff Command Specification
## Purpose
The `openspec diff` command provides developers with a visual comparison between proposed spec changes and the current deployed specs.
## Command Syntax
```bash
openspec diff [change-name]
```
## Behavior
### Requirement: Without Arguments
The command SHALL provide an interactive selection when no change is specified.
#### Scenario: Running without arguments
- **WHEN** running `openspec diff` without arguments
- **THEN** list all available changes in the `changes/` directory (excluding archive)
- **AND** prompt user to select a change
### Requirement: With Change Name
The command SHALL compare specs when a specific change is provided.
#### Scenario: Running with change name
- **WHEN** running `openspec diff <change-name>`
- **THEN** compare all spec files in `changes/<change-name>/specs/` with corresponding files in `specs/`
### Requirement: Diff Output
The command SHALL generate appropriate diff output for all spec changes.
#### Scenario: Comparing existing files
- **WHEN** file exists in both locations
- **THEN** show unified diff
#### Scenario: New files
- **WHEN** file only exists in change
- **THEN** show as new file (all lines with +)
#### Scenario: Deleted files
- **WHEN** file only exists in current specs
- **THEN** show as deleted (all lines with -)
### Requirement: Display Format
The command SHALL use standard unified diff format for consistency with existing tools.
#### Scenario: Formatting diff output
- **WHEN** displaying diff output
- **THEN** use standard unified diff format:
- Lines prefixed with `-` for removed content
- Lines prefixed with `+` for added content
- Lines without prefix for unchanged context
- File headers showing the paths being compared
### Requirement: Color Support
The command SHALL enhance readability with colors when supported.
#### Scenario: Terminal with color support
- **WHEN** terminal supports colors
- **THEN** display:
- Removed lines in red
- Added lines in green
- File headers in bold
- Context lines in default color
### Requirement: Error Handling
The command SHALL provide clear error messages for various failure conditions.
#### Scenario: Change not found
- **WHEN** specified change doesn't exist
- **THEN** display error "Change '<name>' not found"
#### Scenario: No specs in change
- **WHEN** no specs directory in change
- **THEN** display "No spec changes found for '<name>'"
#### Scenario: Missing changes directory
- **WHEN** changes directory doesn't exist
- **THEN** display "No OpenSpec changes directory found"
## Examples
```bash
# View diff for specific change
$ openspec diff add-auth-feature
--- specs/user-auth/spec.md
+++ changes/add-auth-feature/specs/user-auth/spec.md
@@ -10,6 +10,8 @@
Users SHALL authenticate with email and password.
+Users MAY authenticate with OAuth providers.
+
WHEN credentials are valid THEN issue JWT token.
# List all changes and select
$ openspec diff
Available changes:
1. add-auth-feature
2. update-payment-flow
3. add-status-command
Select a change (1-3):
```
+110 -62
View File
@@ -6,20 +6,28 @@ The `openspec init` command SHALL create a complete OpenSpec directory structure
## Behavior
### Progress Indicators
### Requirement: 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"
The command SHALL display progress indicators during initialization to provide clear feedback about each step.
### Directory Creation
#### Scenario: Displaying initialization progress
WHEN `openspec init` is executed
THEN create the following directory structure:
- **WHEN** executing initialization steps
- **THEN** validate environment silently in background (no output unless error)
- **AND** display progress with ora spinners:
- Show spinner: "⠋ Creating OpenSpec structure..."
- Then success: "✔ OpenSpec structure created"
- Show spinner: "⠋ Configuring AI tools..."
- Then success: "✔ AI tools configured"
### Requirement: Directory Creation
The command SHALL create the complete OpenSpec directory structure with all required directories and files.
#### Scenario: Creating OpenSpec structure
- **WHEN** `openspec init` is executed
- **THEN** create the following directory structure:
```
openspec/
├── project.md
@@ -29,27 +37,41 @@ openspec/
└── archive/
```
### File Generation
### Requirement: File Generation
The command SHALL generate:
- `README.md` containing complete OpenSpec instructions for AI assistants
- `project.md` with project context template
The command SHALL generate required template files with appropriate content for immediate use.
### AI Tool Configuration
#### Scenario: Generating template files
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)
- **WHEN** initializing OpenSpec
- **THEN** generate `README.md` containing complete OpenSpec instructions for AI assistants
- **AND** generate `project.md` with project context template
### AI Tool Configuration Details
### Requirement: AI Tool Configuration
WHEN Claude Code is selected
THEN create or update `CLAUDE.md` in the project root directory (not inside openspec/)
The command SHALL configure AI coding assistants with OpenSpec instructions based on user selection.
WHEN CLAUDE.md does not exist
THEN create new file with OpenSpec content wrapped in markers:
#### Scenario: Prompting for AI tool selection
- **WHEN** run interactively
- **THEN** prompt user to select AI tools to configure:
- Claude Code (updates/creates CLAUDE.md with OpenSpec markers)
- Cursor (future)
- Aider (future)
### Requirement: AI Tool Configuration Details
The command SHALL properly configure selected AI tools with OpenSpec-specific instructions using a marker system.
#### Scenario: Configuring Claude Code
- **WHEN** Claude Code is selected
- **THEN** create or update `CLAUDE.md` in the project root directory (not inside openspec/)
#### Scenario: Creating new CLAUDE.md
- **WHEN** CLAUDE.md does not exist
- **THEN** create new file with OpenSpec content wrapped in markers:
```markdown
<!-- OPENSPEC:START -->
# OpenSpec Project
@@ -62,51 +84,71 @@ 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
#### Scenario: Updating existing CLAUDE.md
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
- **WHEN** CLAUDE.md already exists
- **THEN** preserve all existing content
- **AND** insert OpenSpec content at the beginning of the file using markers
- **AND** ensure markers don't duplicate if they already exist
#### Scenario: Managing content with markers
- **WHEN** using the marker system
- **THEN** use `<!-- OPENSPEC:START -->` to mark the beginning of managed content
- **AND** use `<!-- OPENSPEC:END -->` to mark the end of managed content
- **AND** allow OpenSpec to update its content without affecting user customizations
- **AND** preserve all content outside the markers intact
WHY use markers:
- Users may have existing CLAUDE.md instructions they want to keep
- OpenSpec can update its instructions in future versions
- Clear boundary between OpenSpec-managed and user-managed content
### Interactive Mode
### Requirement: 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)
The command SHALL provide an interactive menu for AI tool selection with clear navigation instructions.
User navigation:
- Use arrow keys to move between options
- Press Enter to select the highlighted option
#### Scenario: Displaying interactive menu
### Safety Checks
- **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)
WHEN `openspec/` directory already exists
THEN display error with ora fail indicator:
"✖ Error: OpenSpec seems to already be initialized. Use 'openspec update' to update the structure."
#### Scenario: Navigating the menu
WHEN checking initialization feasibility
THEN verify write permissions in the target directory silently
AND only display error if permissions are insufficient
- **WHEN** user is in the menu
- **THEN** allow arrow keys to move between options
- **AND** allow Enter key to select the highlighted option
### Success Output
### Requirement: Safety Checks
WHEN initialization completes successfully
THEN display actionable prompts for AI-driven workflow:
The command SHALL perform safety checks to prevent overwriting existing structures and ensure proper permissions.
#### Scenario: Detecting existing initialization
- **WHEN** `openspec/` directory already exists
- **THEN** display error with ora fail indicator:
- "✖ Error: OpenSpec seems to already be initialized. Use 'openspec update' to update the structure."
#### Scenario: Checking write permissions
- **WHEN** checking initialization feasibility
- **THEN** verify write permissions in the target directory silently
- **AND** only display error if permissions are insufficient
### Requirement: Success Output
The command SHALL provide clear, actionable next steps upon successful initialization.
#### Scenario: Displaying success message
- **WHEN** initialization completes successfully
- **THEN** display actionable prompts for AI-driven workflow:
```
✔ OpenSpec initialized successfully!
@@ -132,12 +174,18 @@ The prompts SHALL:
- Guide users through the AI-driven workflow
- Replace placeholder text ([YOUR FEATURE HERE]) with actual features
### Exit Codes
### Requirement: 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)
The command SHALL use consistent exit codes to indicate different failure modes.
#### Scenario: Returning exit codes
- **WHEN** the command completes
- **THEN** return appropriate exit code:
- 0: Success
- 1: General error (including when OpenSpec directory already exists)
- 2: Insufficient permissions (reserved for future use)
- 3: User cancelled operation (reserved for future use)
## Why
+96
View File
@@ -0,0 +1,96 @@
# List Command Specification
## Purpose
The `openspec list` command SHALL provide developers with a quick overview of all active changes in the project, showing their names and task completion status.
## Behavior
### Requirement: Command Execution
The command SHALL scan and analyze all active changes to provide a comprehensive overview.
#### Scenario: Scanning for changes
- **WHEN** `openspec list` is executed
- **THEN** scan the `openspec/changes/` directory for change directories
- **AND** exclude the `archive/` subdirectory from results
- **AND** parse each change's `tasks.md` file to count task completion
### Requirement: Task Counting
The command SHALL accurately count task completion status using standard markdown checkbox patterns.
#### Scenario: Counting tasks in tasks.md
- **WHEN** parsing a `tasks.md` file
- **THEN** count tasks matching these patterns:
- Completed: Lines containing `- [x]`
- Incomplete: Lines containing `- [ ]`
- **AND** calculate total tasks as the sum of completed and incomplete
### Requirement: Output Format
The command SHALL display changes in a clear, readable table format with progress indicators.
#### Scenario: Displaying change list
- **WHEN** displaying the list
- **THEN** show a table with columns:
- Change name (directory name)
- Task progress (e.g., "3/5 tasks" or "✓ Complete")
- **AND** use status indicators:
- `✓` for fully completed changes (all tasks done)
- Progress fraction for partial completion
Example output:
```
Changes:
add-auth-feature 3/5 tasks
update-api-docs ✓ Complete
fix-validation 0/2 tasks
add-list-command 1/4 tasks
```
### Requirement: Empty State
The command SHALL provide clear feedback when no active changes are present.
#### Scenario: Handling empty state
- **WHEN** no active changes exist (only archive/ or empty changes/)
- **THEN** display: "No active changes found."
### Requirement: Error Handling
The command SHALL gracefully handle missing files and directories with appropriate messages.
#### Scenario: Missing tasks.md file
- **WHEN** a change directory has no `tasks.md` file
- **THEN** display the change with "No tasks" status
#### Scenario: Missing changes directory
- **WHEN** `openspec/changes/` directory doesn't exist
- **THEN** display error: "No OpenSpec changes directory found. Run 'openspec init' first."
- **AND** exit with code 1
### Requirement: Sorting
The command SHALL maintain consistent ordering of changes for predictable output.
#### Scenario: Ordering changes
- **WHEN** displaying multiple changes
- **THEN** sort them in alphabetical order by change name
## Why
Developers need a quick way to:
- See what changes are in progress
- Identify which changes are ready to archive
- Understand the overall project evolution status
- Get a bird's-eye view without opening multiple files
This command provides that visibility with minimal effort, following OpenSpec's philosophy of simplicity and clarity.
+52 -27
View File
@@ -6,45 +6,70 @@ As a developer using OpenSpec, I want to update the OpenSpec instructions in my
## Core Requirements
### Update Behavior
### Requirement: Update Behavior
The update command SHALL update OpenSpec instruction files to the latest templates.
The update command SHALL update OpenSpec instruction files to the latest templates in a team-friendly manner.
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"
#### Scenario: Running update command
### Prerequisites
- **WHEN** a user runs `openspec update`
- **THEN** the command SHALL:
- Check if the `openspec` directory exists
- Replace `openspec/README.md` with the latest template (complete replacement)
- Update **only existing** AI tool configuration files (e.g., CLAUDE.md)
- Check each registered AI tool configurator
- For each configurator, check if its file exists
- Update only files that already exist using their markers
- Preserve user content outside markers
- **Never create new AI tool configuration files**
- Display success message listing updated files
The command SHALL require:
- An existing `openspec` directory (created by `openspec init`)
### Requirement: Prerequisites
IF the `openspec` directory does not exist THEN:
- Display error: "No OpenSpec directory found. Run 'openspec init' first."
- Exit with code 1
The command SHALL require an existing OpenSpec structure before allowing updates.
### File Handling
#### Scenario: Checking prerequisites
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)
- **GIVEN** the command requires an existing `openspec` directory (created by `openspec init`)
- **WHEN** the `openspec` directory does not exist
- **THEN** display error: "No OpenSpec directory found. Run 'openspec init' first."
- **AND** exit with code 1
### Requirement: File Handling
The update command SHALL handle file updates in a predictable and safe manner.
#### Scenario: Updating files
- **WHEN** updating files
- **THEN** completely replace `openspec/README.md` with the latest template
- **AND** update only the OpenSpec-managed blocks in **existing** AI tool files using markers
- **AND** use the default directory name `openspec`
- **AND** be idempotent (repeated runs have no additional effect)
- **AND** respect team members' AI tool choices by not creating unwanted files
## Edge Cases
### File Permissions
IF file write fails THEN let the error bubble up naturally with file path.
### Requirement: Error Handling
### Missing CLAUDE.md
IF CLAUDE.md doesn't exist THEN create it with the template content.
The command SHALL handle edge cases gracefully.
### Custom Directory Name
Not supported in this change. The default directory name `openspec` SHALL be used.
#### Scenario: File permission errors
- **WHEN** file write fails
- **THEN** let the error bubble up naturally with file path
#### Scenario: Missing AI tool files
- **WHEN** an AI tool configuration file doesn't exist
- **THEN** skip updating that file
- **AND** do not create it
#### Scenario: Custom directory names
- **WHEN** considering custom directory names
- **THEN** not supported in this change
- **AND** the default directory name `openspec` SHALL be used
## Success Criteria
+153 -15
View File
@@ -14,8 +14,14 @@ The system SHALL follow these principles:
## Directory Structure
WHEN an OpenSpec project is initialized
THEN it SHALL have this structure:
### Requirement: Project Structure
An OpenSpec project SHALL maintain a consistent directory structure for specifications and changes.
#### Scenario: Initializing project structure
- **WHEN** an OpenSpec project is initialized
- **THEN** it SHALL have this structure:
```
openspec/
├── project.md # Project-specific context
@@ -36,23 +42,144 @@ openspec/
└── YYYY-MM-DD-[name]/
```
## Specification Format
### Requirement: Structured Format for Behavioral Specs
Behavioral specifications SHALL use a structured format with consistent section headers and keywords to ensure visual consistency and parseability.
#### Scenario: Writing requirement sections
- **WHEN** documenting a requirement in a behavioral specification
- **THEN** use a level-3 heading with format `### Requirement: [Name]`
- **AND** immediately follow with a SHALL statement describing core behavior
- **AND** keep requirement names descriptive and under 50 characters
#### Scenario: Documenting scenarios
- **WHEN** documenting specific behaviors or use cases
- **THEN** use level-4 headings with format `#### Scenario: [Description]`
- **AND** use bullet points with bold keywords for steps:
- **GIVEN** for initial state (optional)
- **WHEN** for conditions or triggers
- **THEN** for expected outcomes
- **AND** for additional outcomes or conditions
#### Scenario: Adding implementation details
- **WHEN** a step requires additional detail
- **THEN** use sub-bullets under the main step
- **AND** maintain consistent indentation
- Sub-bullets provide examples or specifics
- Keep sub-bullets concise
## Change Storage Convention
### Future State Storage
### Requirement: Header-Based Requirement Identification
Requirement headers SHALL serve as unique identifiers for programmatic matching between current specs and proposed changes.
#### Scenario: Matching requirements programmatically
- **WHEN** processing delta changes
- **THEN** use the `### Requirement: [Name]` header as the unique identifier
- **AND** match using normalized headers: `normalize(header) = trim(header)`
- **AND** compare headers with case-sensitive equality after normalization
#### Scenario: Handling requirement renames
- **WHEN** renaming a requirement
- **THEN** use a special `## RENAMED Requirements` section
- **AND** specify both old and new names explicitly:
```markdown
## RENAMED Requirements
- FROM: `### Requirement: Old Name`
- TO: `### Requirement: New Name`
```
- **AND** if content also changes, include under MODIFIED using the NEW header
#### Scenario: Validating header uniqueness
- **WHEN** creating or modifying requirements
- **THEN** ensure no duplicate headers exist within a spec
- **AND** validation tools SHALL flag duplicate headers as errors
### Requirement: Change Storage Convention
Change proposals SHALL store only the additions, modifications, and removals to specifications, not complete future states.
#### Scenario: Creating change proposals with additions
- **WHEN** creating a change proposal that adds new requirements
- **THEN** include only the new requirements under `## ADDED Requirements`
- **AND** each requirement SHALL include its complete content
- **AND** use the standard structured format for requirements and scenarios
#### Scenario: Creating change proposals with modifications
- **WHEN** creating a change proposal that modifies existing requirements
- **THEN** include the modified requirements under `## MODIFIED Requirements`
- **AND** use the same header text as in the current spec (normalized)
- **AND** include the complete modified requirement (not a diff)
- **AND** optionally annotate what changed with inline comments like `← (was X)`
#### Scenario: Creating change proposals with removals
- **WHEN** creating a change proposal that removes requirements
- **THEN** list them under `## REMOVED Requirements`
- **AND** use the normalized header text for identification
- **AND** include reason for removal
- **AND** document any migration path if applicable
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
- Delta files showing only what changes
- Sections for ADDED, MODIFIED, REMOVED, and RENAMED requirements
- Normalized header matching for requirement identification
- Complete requirements using the structured format
- Clear indication of change type for each requirement
### Proposal Format
#### Scenario: Using standard output symbols
WHEN documenting what changes
THEN the proposal SHALL explicitly describe each change:
- **WHEN** displaying delta operations in CLI output
- **THEN** use these standard symbols:
- `+` for ADDED (green)
- `~` for MODIFIED (yellow)
- `-` for REMOVED (red)
- `→` for RENAMED (cyan)
### Requirement: Archive Process Enhancement
The archive process SHALL programmatically apply delta changes to current specifications using header-based matching.
#### Scenario: Archiving changes with deltas
- **WHEN** archiving a completed change
- **THEN** the archive command SHALL:
1. Parse RENAMED sections first and apply renames
2. Parse REMOVED sections and remove by normalized header match
3. Parse MODIFIED sections and replace by normalized header match (using new names if renamed)
4. Parse ADDED sections and append new requirements
- **AND** validate that all MODIFIED/REMOVED headers exist in current spec
- **AND** validate that ADDED headers don't already exist
- **AND** generate the updated spec in the main specs/ directory
#### Scenario: Handling conflicts during archive
- **WHEN** delta changes conflict with current spec state
- **THEN** the archive command SHALL report specific conflicts
- **AND** require manual resolution before proceeding
- **AND** provide clear guidance on resolving conflicts
### Requirement: Proposal Format
Proposals SHALL explicitly document all changes with clear from/to comparisons.
#### Scenario: Documenting changes
- **WHEN** documenting what changes
- **THEN** the proposal SHALL explicitly describe each change:
```markdown
**[Section or Behavior Name]**
@@ -78,8 +205,14 @@ The change process SHALL follow these states:
## Viewing Changes
WHEN reviewing proposed changes
THEN reviewers can compare using:
### Requirement: Change Review
The system SHALL support multiple methods for reviewing proposed changes.
#### Scenario: Reviewing changes
- **WHEN** reviewing proposed changes
- **THEN** reviewers can compare using:
- GitHub PR diff view when changes are committed
- Command line: `diff -u specs/[capability]/spec.md changes/[name]/specs/[capability]/spec.md`
- Any visual diff tool comparing current vs future state
@@ -117,4 +250,9 @@ Clean future state storage provides:
- **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
- **Clear intent**: Explicit proposals document reasoning
The structured format adds:
- **Visual Consistency**: Requirement and Scenario prefixes make sections instantly recognizable
- **Parseability**: Consistent structure enables tooling and automation
- **Gradual Adoption**: Existing specs can migrate incrementally
+32
View File
@@ -5,6 +5,8 @@ import { promises as fs } from 'fs';
import { InitCommand } from '../core/init.js';
import { UpdateCommand } from '../core/update.js';
import { DiffCommand } from '../core/diff.js';
import { ListCommand } from '../core/list.js';
import { ArchiveCommand } from '../core/archive.js';
const program = new Command();
@@ -75,4 +77,34 @@ program
}
});
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
.command('archive [change-name]')
.description('Archive a completed change and update main specs')
.option('-y, --yes', 'Skip confirmation prompts')
.option('--skip-specs', 'Skip spec update operations (useful for infrastructure, tooling, or doc-only changes)')
.action(async (changeName?: string, options?: { yes?: boolean; skipSpecs?: boolean }) => {
try {
const archiveCommand = new ArchiveCommand();
await archiveCommand.execute(changeName, options);
} catch (error) {
console.log(); // Empty line for spacing
ora().fail(`Error: ${(error as Error).message}`);
process.exit(1);
}
});
program.parse();
+231
View File
@@ -0,0 +1,231 @@
import { promises as fs } from 'fs';
import path from 'path';
import { select, confirm } from '@inquirer/prompts';
import { FileSystemUtils } from '../utils/file-system.js';
interface SpecUpdate {
source: string;
target: string;
exists: boolean;
}
export class ArchiveCommand {
async execute(changeName?: string, options: { yes?: boolean; skipSpecs?: boolean } = {}): Promise<void> {
const targetPath = '.';
const changesDir = path.join(targetPath, 'openspec', 'changes');
const archiveDir = path.join(changesDir, 'archive');
const mainSpecsDir = path.join(targetPath, 'openspec', 'specs');
// Check if changes directory exists
try {
await fs.access(changesDir);
} catch {
throw new Error("No OpenSpec changes directory found. Run 'openspec init' first.");
}
// Get change name interactively if not provided
if (!changeName) {
const selectedChange = await this.selectChange(changesDir);
if (!selectedChange) {
console.log('No change selected. Aborting.');
return;
}
changeName = selectedChange;
}
const changeDir = path.join(changesDir, changeName);
// Verify change exists
try {
const stat = await fs.stat(changeDir);
if (!stat.isDirectory()) {
throw new Error(`Change '${changeName}' not found.`);
}
} catch {
throw new Error(`Change '${changeName}' not found.`);
}
// Check for incomplete tasks
const tasksPath = path.join(changeDir, 'tasks.md');
const incompleteTasks = await this.checkIncompleteTasks(tasksPath);
if (incompleteTasks > 0) {
if (!options.yes) {
const proceed = await confirm({
message: `Warning: ${incompleteTasks} incomplete task(s) found. Continue?`,
default: false
});
if (!proceed) {
console.log('Archive cancelled.');
return;
}
} else {
console.log(`Warning: ${incompleteTasks} incomplete task(s) found. Continuing due to --yes flag.`);
}
}
// Handle spec updates unless skipSpecs flag is set
if (options.skipSpecs) {
console.log('Skipping spec updates (--skip-specs flag provided).');
} else {
// Find specs to update
const specUpdates = await this.findSpecUpdates(changeDir, mainSpecsDir);
if (specUpdates.length > 0) {
console.log('\nSpecs to update:');
for (const update of specUpdates) {
const status = update.exists ? 'update' : 'create';
const capability = path.basename(path.dirname(update.target));
console.log(` ${capability}: ${status}`);
}
let shouldUpdateSpecs = true;
if (!options.yes) {
shouldUpdateSpecs = await confirm({
message: 'Proceed with spec updates?',
default: true
});
if (!shouldUpdateSpecs) {
console.log('Skipping spec updates. Proceeding with archive.');
}
}
if (shouldUpdateSpecs) {
// Update specs
for (const update of specUpdates) {
await this.updateSpec(update);
}
console.log('Specs updated successfully.');
}
}
}
// Create archive directory with date prefix
const archiveName = `${this.getArchiveDate()}-${changeName}`;
const archivePath = path.join(archiveDir, archiveName);
// Check if archive already exists
try {
await fs.access(archivePath);
throw new Error(`Archive '${archiveName}' already exists.`);
} catch (error: any) {
if (error.code !== 'ENOENT') {
throw error;
}
}
// Create archive directory if needed
await fs.mkdir(archiveDir, { recursive: true });
// Move change to archive
await fs.rename(changeDir, archivePath);
console.log(`Change '${changeName}' archived as '${archiveName}'.`);
}
private async selectChange(changesDir: string): Promise<string | null> {
// Get all directories in changes (excluding archive)
const entries = await fs.readdir(changesDir, { withFileTypes: true });
const changeDirs = entries
.filter(entry => entry.isDirectory() && entry.name !== 'archive')
.map(entry => entry.name)
.sort();
if (changeDirs.length === 0) {
console.log('No active changes found.');
return null;
}
console.log('Available changes:');
const choices = changeDirs.map(name => ({
name: name,
value: name
}));
try {
const answer = await select({
message: 'Select a change to archive',
choices
});
return answer;
} catch (error) {
// User cancelled (Ctrl+C)
return null;
}
}
private async checkIncompleteTasks(tasksPath: string): Promise<number> {
try {
const content = await fs.readFile(tasksPath, 'utf-8');
const lines = content.split('\n');
let incompleteTasks = 0;
for (const line of lines) {
if (line.includes('- [ ]')) {
incompleteTasks++;
}
}
return incompleteTasks;
} catch {
// No tasks.md file or error reading it
return 0;
}
}
private async findSpecUpdates(changeDir: string, mainSpecsDir: string): Promise<SpecUpdate[]> {
const updates: SpecUpdate[] = [];
const changeSpecsDir = path.join(changeDir, 'specs');
try {
const entries = await fs.readdir(changeSpecsDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) {
const specFile = path.join(changeSpecsDir, entry.name, 'spec.md');
const targetFile = path.join(mainSpecsDir, entry.name, 'spec.md');
try {
await fs.access(specFile);
// Check if target exists
let exists = false;
try {
await fs.access(targetFile);
exists = true;
} catch {
exists = false;
}
updates.push({
source: specFile,
target: targetFile,
exists
});
} catch {
// Source spec doesn't exist, skip
}
}
}
} catch {
// No specs directory in change
}
return updates;
}
private async updateSpec(update: SpecUpdate): Promise<void> {
// Create target directory if needed
const targetDir = path.dirname(update.target);
await fs.mkdir(targetDir, { recursive: true });
// Copy spec file
const content = await fs.readFile(update.source, 'utf-8');
await fs.writeFile(update.target, content);
}
private getArchiveDate(): string {
// Returns date in YYYY-MM-DD format
return new Date().toISOString().split('T')[0];
}
}
+1 -1
View File
@@ -82,7 +82,7 @@ export class DiffCommand {
choices
});
return answer;
return answer as string;
}
private async showDiffs(changeSpecsDir: string): Promise<void> {
+1 -1
View File
@@ -64,7 +64,7 @@ export class InitCommand {
}))
});
config.aiTools = [selectedTool];
config.aiTools = [selectedTool as string];
return config;
}
+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}`);
}
}
}
+60 -15
View File
@@ -43,9 +43,9 @@ openspec/
│ │ ├── proposal.md # Why, what, impact (consolidated)
│ │ ├── tasks.md # Implementation checklist
│ │ ├── design.md # Technical decisions (optional, for complex changes)
│ │ └── specs/ # Future state of affected specs
│ │ └── specs/ # Delta changes to specs
│ │ └── [capability]/
│ │ └── spec.md # Clean markdown (no diff syntax)
│ │ └── spec.md # Delta format (ADDED/MODIFIED/REMOVED/RENAMED)
│ └── archive/ # Completed changes (dated)
\`\`\`
@@ -94,7 +94,35 @@ Before any task:
- 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
### 3. Delta-Based Change Format
Changes use a delta format with clear sections:
\`\`\`markdown
## ADDED Requirements
### Requirement: New Feature
[Complete requirement content in structured format]
## MODIFIED Requirements
### Requirement: Existing Feature
[Complete modified requirement (header must match current spec)]
## REMOVED Requirements
### Requirement: Old Feature
**Reason for removal**: [Why removing]
**Migration path**: [How to handle existing usage]
## RENAMED Requirements
- FROM: \`### Requirement: Old Name\`
- TO: \`### Requirement: New Name\`
\`\`\`
Key rules:
- Headers are matched using \`normalize(header) = trim(header)\`
- Include complete requirements (not diffs)
- Use standard symbols in CLI output: + (added), ~ (modified), - (removed), → (renamed)
### 4. Creating a Change Proposal
When a user requests a significant change:
@@ -113,13 +141,21 @@ openspec/changes/[descriptive-name]/
- 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
# 3. Create delta specs for ALL affected capabilities
# - Store only the changes (not complete future state)
# - Use sections: ## ADDED, ## MODIFIED, ## REMOVED, ## RENAMED
# - Include complete requirements in their final form
# Example spec.md content:
# ## ADDED Requirements
# ### Requirement: Password Reset
# Users SHALL be able to reset passwords via email...
#
# ## MODIFIED Requirements
# ### Requirement: User Authentication
# [Complete modified requirement with new password reset hook]
specs/
└── [capability]/
└── spec.md
└── spec.md # Contains delta sections
# 4. Create tasks.md with implementation steps
## 1. [Task Group]
@@ -130,16 +166,16 @@ specs/
[Technical decisions and trade-offs]
\`\`\`
### 4. The Change Lifecycle
### 5. The Change Lifecycle
1. **Propose** → Create change directory with all documentation
1. **Propose** → Create change directory with delta-based documentation
2. **Review** → User reviews and approves the proposal
3. **Implement** → Follow the approved tasks.md (can be multiple PRs)
4. **Deploy** → User confirms deployment
5. **Update Specs** → Sync specs/ with new reality (IF the change affects system capabilities)
5. **Update Specs** → Apply deltas to sync specs/ with new reality (IF the change affects system capabilities)
6. **Archive** → Move to \`changes/archive/YYYY-MM-DD-[name]/\`
### 5. Implementing Changes
### 6. Implementing Changes
When implementing an approved change:
1. Follow the tasks.md checklist exactly
@@ -154,7 +190,7 @@ When implementing an approved change:
- 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
### 7. Updating Specs and Archiving After Deployment
**Create a separate PR after deployment** that:
1. Moves change to \`changes/archive/YYYY-MM-DD-[name]/\`
@@ -163,7 +199,7 @@ When implementing an approved change:
This ensures changes are only archived when truly complete and deployed.
### 7. Types of Changes That Don't Require Specs
### 8. Types of Changes That Don't Require Specs
Some changes only affect development infrastructure and don't need specs:
- Initial project setup (package.json, tsconfig.json, etc.)
@@ -216,7 +252,16 @@ User: "Add password reset functionality"
You should:
1. Read specs/user-auth/spec.md
2. Check changes/ for pending auth changes
3. Create changes/add-password-reset/ with proposal
3. Create changes/add-password-reset/ with:
- proposal.md describing the change
- specs/user-auth/spec.md with:
## ADDED Requirements
### Requirement: Password Reset
[Complete requirement for password reset]
## MODIFIED Requirements
### Requirement: User Authentication
[Updated to integrate with password reset]
4. Wait for approval before implementing
\`\`\`
+32 -12
View File
@@ -1,8 +1,8 @@
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 { OPENSPEC_DIR_NAME } from './config.js';
import { readmeTemplate } from './templates/readme-template.js';
import { ToolRegistry } from './configurators/registry.js';
export class UpdateCommand {
async execute(projectPath: string): Promise<void> {
@@ -19,17 +19,37 @@ export class UpdateCommand {
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
);
// 3. Update existing AI tool configuration files only
const configurators = ToolRegistry.getAll();
let updatedFiles: string[] = [];
let failedFiles: string[] = [];
for (const configurator of configurators) {
const configFilePath = path.join(resolvedProjectPath, configurator.configFileName);
// Only update if the file already exists
if (await FileSystemUtils.fileExists(configFilePath)) {
try {
await configurator.configure(resolvedProjectPath, openspecPath);
updatedFiles.push(configurator.configFileName);
} catch (error) {
failedFiles.push(configurator.configFileName);
console.error(`Failed to update ${configurator.configFileName}: ${error instanceof Error ? error.message : String(error)}`);
}
}
}
// 4. Success message (ASCII-safe)
console.log('Updated OpenSpec instructions');
const messages: string[] = ['Updated OpenSpec instructions (README.md)'];
if (updatedFiles.length > 0) {
messages.push(`Updated AI tool files: ${updatedFiles.join(', ')}`);
}
if (failedFiles.length > 0) {
messages.push(`Failed to update: ${failedFiles.join(', ')}`);
}
console.log(messages.join('\n'));
}
}
+339
View File
@@ -0,0 +1,339 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { ArchiveCommand } from '../../src/core/archive.js';
import { promises as fs } from 'fs';
import path from 'path';
import os from 'os';
// Mock @inquirer/prompts
vi.mock('@inquirer/prompts', () => ({
select: vi.fn(),
confirm: vi.fn()
}));
describe('ArchiveCommand', () => {
let tempDir: string;
let archiveCommand: ArchiveCommand;
const originalConsoleLog = console.log;
beforeEach(async () => {
// Create temp directory
tempDir = path.join(os.tmpdir(), `openspec-archive-test-${Date.now()}`);
await fs.mkdir(tempDir, { recursive: true });
// Change to temp directory
process.chdir(tempDir);
// Create OpenSpec structure
const openspecDir = path.join(tempDir, 'openspec');
await fs.mkdir(path.join(openspecDir, 'changes'), { recursive: true });
await fs.mkdir(path.join(openspecDir, 'specs'), { recursive: true });
await fs.mkdir(path.join(openspecDir, 'changes', 'archive'), { recursive: true });
// Suppress console.log during tests
console.log = vi.fn();
archiveCommand = new ArchiveCommand();
});
afterEach(async () => {
// Restore console.log
console.log = originalConsoleLog;
// Clear mocks
vi.clearAllMocks();
// Clean up temp directory
try {
await fs.rm(tempDir, { recursive: true, force: true });
} catch (error) {
// Ignore cleanup errors
}
});
describe('execute', () => {
it('should archive a change successfully', async () => {
// Create a test change
const changeName = 'test-feature';
const changeDir = path.join(tempDir, 'openspec', 'changes', changeName);
await fs.mkdir(changeDir, { recursive: true });
// Create tasks.md with completed tasks
const tasksContent = '- [x] Task 1\n- [x] Task 2';
await fs.writeFile(path.join(changeDir, 'tasks.md'), tasksContent);
// Execute archive with --yes flag
await archiveCommand.execute(changeName, { yes: true });
// Check that change was moved to archive
const archiveDir = path.join(tempDir, 'openspec', 'changes', 'archive');
const archives = await fs.readdir(archiveDir);
expect(archives.length).toBe(1);
expect(archives[0]).toMatch(new RegExp(`\\d{4}-\\d{2}-\\d{2}-${changeName}`));
// Verify original change directory no longer exists
await expect(fs.access(changeDir)).rejects.toThrow();
});
it('should warn about incomplete tasks', async () => {
const changeName = 'incomplete-feature';
const changeDir = path.join(tempDir, 'openspec', 'changes', changeName);
await fs.mkdir(changeDir, { recursive: true });
// Create tasks.md with incomplete tasks
const tasksContent = '- [x] Task 1\n- [ ] Task 2\n- [ ] Task 3';
await fs.writeFile(path.join(changeDir, 'tasks.md'), tasksContent);
// Execute archive with --yes flag
await archiveCommand.execute(changeName, { yes: true });
// Verify warning was logged
expect(console.log).toHaveBeenCalledWith(
expect.stringContaining('Warning: 2 incomplete task(s) found')
);
});
it('should update specs when archiving', async () => {
const changeName = 'spec-feature';
const changeDir = path.join(tempDir, 'openspec', 'changes', changeName);
const changeSpecDir = path.join(changeDir, 'specs', 'test-capability');
await fs.mkdir(changeSpecDir, { recursive: true });
// Create spec in change
const specContent = '# Test Capability Spec\n\nTest content';
await fs.writeFile(path.join(changeSpecDir, 'spec.md'), specContent);
// Execute archive with --yes flag
await archiveCommand.execute(changeName, { yes: true });
// Verify spec was copied to main specs
const mainSpecPath = path.join(tempDir, 'openspec', 'specs', 'test-capability', 'spec.md');
const copiedContent = await fs.readFile(mainSpecPath, 'utf-8');
expect(copiedContent).toBe(specContent);
});
it('should throw error if change does not exist', async () => {
await expect(
archiveCommand.execute('non-existent-change', { yes: true })
).rejects.toThrow("Change 'non-existent-change' not found.");
});
it('should throw error if archive already exists', async () => {
const changeName = 'duplicate-feature';
const changeDir = path.join(tempDir, 'openspec', 'changes', changeName);
await fs.mkdir(changeDir, { recursive: true });
// Create existing archive with same date
const date = new Date().toISOString().split('T')[0];
const archivePath = path.join(tempDir, 'openspec', 'changes', 'archive', `${date}-${changeName}`);
await fs.mkdir(archivePath, { recursive: true });
// Try to archive
await expect(
archiveCommand.execute(changeName, { yes: true })
).rejects.toThrow(`Archive '${date}-${changeName}' already exists.`);
});
it('should handle changes without tasks.md', async () => {
const changeName = 'no-tasks-feature';
const changeDir = path.join(tempDir, 'openspec', 'changes', changeName);
await fs.mkdir(changeDir, { recursive: true });
// Execute archive without tasks.md
await archiveCommand.execute(changeName, { yes: true });
// Should complete without warnings
expect(console.log).not.toHaveBeenCalledWith(
expect.stringContaining('incomplete task(s)')
);
// Verify change was archived
const archiveDir = path.join(tempDir, 'openspec', 'changes', 'archive');
const archives = await fs.readdir(archiveDir);
expect(archives.length).toBe(1);
});
it('should handle changes without specs', async () => {
const changeName = 'no-specs-feature';
const changeDir = path.join(tempDir, 'openspec', 'changes', changeName);
await fs.mkdir(changeDir, { recursive: true });
// Execute archive without specs
await archiveCommand.execute(changeName, { yes: true });
// Should complete without spec updates
expect(console.log).not.toHaveBeenCalledWith(
expect.stringContaining('Specs to update')
);
// Verify change was archived
const archiveDir = path.join(tempDir, 'openspec', 'changes', 'archive');
const archives = await fs.readdir(archiveDir);
expect(archives.length).toBe(1);
});
it('should skip spec updates when --skip-specs flag is used', async () => {
const changeName = 'skip-specs-feature';
const changeDir = path.join(tempDir, 'openspec', 'changes', changeName);
const changeSpecDir = path.join(changeDir, 'specs', 'test-capability');
await fs.mkdir(changeSpecDir, { recursive: true });
// Create spec in change
const specContent = '# Test Capability Spec\n\nTest content';
await fs.writeFile(path.join(changeSpecDir, 'spec.md'), specContent);
// Execute archive with --skip-specs flag
await archiveCommand.execute(changeName, { yes: true, skipSpecs: true });
// Verify skip message was logged
expect(console.log).toHaveBeenCalledWith(
'Skipping spec updates (--skip-specs flag provided).'
);
// Verify spec was NOT copied to main specs
const mainSpecPath = path.join(tempDir, 'openspec', 'specs', 'test-capability', 'spec.md');
await expect(fs.access(mainSpecPath)).rejects.toThrow();
// Verify change was still archived
const archiveDir = path.join(tempDir, 'openspec', 'changes', 'archive');
const archives = await fs.readdir(archiveDir);
expect(archives.length).toBe(1);
expect(archives[0]).toMatch(new RegExp(`\\d{4}-\\d{2}-\\d{2}-${changeName}`));
});
it('should proceed with archive when user declines spec updates', async () => {
const { confirm } = await import('@inquirer/prompts');
const mockConfirm = confirm as unknown as ReturnType<typeof vi.fn>;
const changeName = 'decline-specs-feature';
const changeDir = path.join(tempDir, 'openspec', 'changes', changeName);
const changeSpecDir = path.join(changeDir, 'specs', 'test-capability');
await fs.mkdir(changeSpecDir, { recursive: true });
// Create spec in change
const specContent = '# Test Capability Spec\n\nTest content';
await fs.writeFile(path.join(changeSpecDir, 'spec.md'), specContent);
// Mock confirm to return false (decline spec updates)
mockConfirm.mockResolvedValueOnce(false);
// Execute archive without --yes flag
await archiveCommand.execute(changeName);
// Verify user was prompted about specs
expect(mockConfirm).toHaveBeenCalledWith({
message: 'Proceed with spec updates?',
default: true
});
// Verify skip message was logged
expect(console.log).toHaveBeenCalledWith(
'Skipping spec updates. Proceeding with archive.'
);
// Verify spec was NOT copied to main specs
const mainSpecPath = path.join(tempDir, 'openspec', 'specs', 'test-capability', 'spec.md');
await expect(fs.access(mainSpecPath)).rejects.toThrow();
// Verify change was still archived
const archiveDir = path.join(tempDir, 'openspec', 'changes', 'archive');
const archives = await fs.readdir(archiveDir);
expect(archives.length).toBe(1);
expect(archives[0]).toMatch(new RegExp(`\\d{4}-\\d{2}-\\d{2}-${changeName}`));
});
});
describe('error handling', () => {
it('should throw error when openspec directory does not exist', async () => {
// Remove openspec directory
await fs.rm(path.join(tempDir, 'openspec'), { recursive: true });
await expect(
archiveCommand.execute('any-change', { yes: true })
).rejects.toThrow("No OpenSpec changes directory found. Run 'openspec init' first.");
});
});
describe('interactive mode', () => {
it('should use select prompt for change selection', async () => {
const { select } = await import('@inquirer/prompts');
const mockSelect = select as unknown as ReturnType<typeof vi.fn>;
// Create test changes
const change1 = 'feature-a';
const change2 = 'feature-b';
await fs.mkdir(path.join(tempDir, 'openspec', 'changes', change1), { recursive: true });
await fs.mkdir(path.join(tempDir, 'openspec', 'changes', change2), { recursive: true });
// Mock select to return first change
mockSelect.mockResolvedValueOnce(change1);
// Execute without change name
await archiveCommand.execute(undefined, { yes: true });
// Verify select was called with correct options
expect(mockSelect).toHaveBeenCalledWith({
message: 'Select a change to archive',
choices: [
{ name: change1, value: change1 },
{ name: change2, value: change2 }
]
});
// Verify the selected change was archived
const archiveDir = path.join(tempDir, 'openspec', 'changes', 'archive');
const archives = await fs.readdir(archiveDir);
expect(archives[0]).toContain(change1);
});
it('should use confirm prompt for task warnings', async () => {
const { confirm } = await import('@inquirer/prompts');
const mockConfirm = confirm as unknown as ReturnType<typeof vi.fn>;
const changeName = 'incomplete-interactive';
const changeDir = path.join(tempDir, 'openspec', 'changes', changeName);
await fs.mkdir(changeDir, { recursive: true });
// Create tasks.md with incomplete tasks
const tasksContent = '- [ ] Task 1';
await fs.writeFile(path.join(changeDir, 'tasks.md'), tasksContent);
// Mock confirm to return true (proceed)
mockConfirm.mockResolvedValueOnce(true);
// Execute without --yes flag
await archiveCommand.execute(changeName);
// Verify confirm was called
expect(mockConfirm).toHaveBeenCalledWith({
message: 'Warning: 1 incomplete task(s) found. Continue?',
default: false
});
});
it('should cancel when user declines task warning', async () => {
const { confirm } = await import('@inquirer/prompts');
const mockConfirm = confirm as unknown as ReturnType<typeof vi.fn>;
const changeName = 'cancel-test';
const changeDir = path.join(tempDir, 'openspec', 'changes', changeName);
await fs.mkdir(changeDir, { recursive: true });
// Create tasks.md with incomplete tasks
const tasksContent = '- [ ] Task 1';
await fs.writeFile(path.join(changeDir, 'tasks.md'), tasksContent);
// Mock confirm to return false (cancel)
mockConfirm.mockResolvedValueOnce(false);
// Execute without --yes flag
await archiveCommand.execute(changeName);
// Verify archive was cancelled
expect(console.log).toHaveBeenCalledWith('Archive cancelled.');
// Verify change was not archived
await expect(fs.access(changeDir)).resolves.not.toThrow();
});
});
});
+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);
});
});
});
+165
View File
@@ -0,0 +1,165 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { UpdateCommand } from '../../src/core/update.js';
import { FileSystemUtils } from '../../src/utils/file-system.js';
import { ToolRegistry } from '../../src/core/configurators/registry.js';
import path from 'path';
import fs from 'fs/promises';
import os from 'os';
describe('UpdateCommand', () => {
let testDir: string;
let updateCommand: UpdateCommand;
beforeEach(async () => {
// Create a temporary test directory
testDir = path.join(os.tmpdir(), `openspec-test-${Date.now()}`);
await fs.mkdir(testDir, { recursive: true });
// Create openspec directory
const openspecDir = path.join(testDir, 'openspec');
await fs.mkdir(openspecDir, { recursive: true });
updateCommand = new UpdateCommand();
});
afterEach(async () => {
// Clean up test directory
await fs.rm(testDir, { recursive: true, force: true });
});
it('should update only existing CLAUDE.md file', async () => {
// Create CLAUDE.md file with initial content
const claudePath = path.join(testDir, 'CLAUDE.md');
const initialContent = `# Project Instructions
Some existing content here.
<!-- OPENSPEC:START -->
Old OpenSpec content
<!-- OPENSPEC:END -->
More content after.`;
await fs.writeFile(claudePath, initialContent);
const consoleSpy = vi.spyOn(console, 'log');
// Execute update command
await updateCommand.execute(testDir);
// Check that CLAUDE.md was updated
const updatedContent = await fs.readFile(claudePath, 'utf-8');
expect(updatedContent).toContain('<!-- OPENSPEC:START -->');
expect(updatedContent).toContain('<!-- OPENSPEC:END -->');
expect(updatedContent).toContain('This project uses OpenSpec');
expect(updatedContent).toContain('Some existing content here');
expect(updatedContent).toContain('More content after');
// Check console output
expect(consoleSpy).toHaveBeenCalledWith(
'Updated OpenSpec instructions (README.md)\nUpdated AI tool files: CLAUDE.md'
);
consoleSpy.mockRestore();
});
it('should not create CLAUDE.md if it does not exist', async () => {
// Ensure CLAUDE.md does not exist
const claudePath = path.join(testDir, 'CLAUDE.md');
// Execute update command
await updateCommand.execute(testDir);
// Check that CLAUDE.md was not created
const fileExists = await FileSystemUtils.fileExists(claudePath);
expect(fileExists).toBe(false);
});
it('should handle no AI tool files present', async () => {
// Execute update command with no AI tool files
const consoleSpy = vi.spyOn(console, 'log');
await updateCommand.execute(testDir);
// Should only update OpenSpec instructions
expect(consoleSpy).toHaveBeenCalledWith('Updated OpenSpec instructions (README.md)');
consoleSpy.mockRestore();
});
it('should update multiple AI tool files if present', async () => {
// TODO: When additional configurators are added (Cursor, Aider, etc.),
// enhance this test to create multiple AI tool files and verify
// that all existing files are updated in a single operation.
// For now, we test with just CLAUDE.md.
const claudePath = path.join(testDir, 'CLAUDE.md');
await fs.writeFile(claudePath, '<!-- OPENSPEC:START -->\nOld\n<!-- OPENSPEC:END -->');
const consoleSpy = vi.spyOn(console, 'log');
await updateCommand.execute(testDir);
// Should report updating with new format
expect(consoleSpy).toHaveBeenCalledWith(
'Updated OpenSpec instructions (README.md)\nUpdated AI tool files: CLAUDE.md'
);
consoleSpy.mockRestore();
});
it('should never create new AI tool files', async () => {
// Get all configurators
const configurators = ToolRegistry.getAll();
// Execute update command
await updateCommand.execute(testDir);
// Check that no new AI tool files were created
for (const configurator of configurators) {
const configPath = path.join(testDir, configurator.configFileName);
const fileExists = await FileSystemUtils.fileExists(configPath);
expect(fileExists).toBe(false);
}
});
it('should update README.md in openspec directory', async () => {
// Execute update command
await updateCommand.execute(testDir);
// Check that README.md was created/updated
const readmePath = path.join(testDir, 'openspec', 'README.md');
const fileExists = await FileSystemUtils.fileExists(readmePath);
expect(fileExists).toBe(true);
const content = await fs.readFile(readmePath, 'utf-8');
expect(content).toContain('# OpenSpec Instructions');
});
it('should throw error if openspec directory does not exist', async () => {
// Remove openspec directory
await fs.rm(path.join(testDir, 'openspec'), { recursive: true, force: true });
// Execute update command and expect error
await expect(updateCommand.execute(testDir)).rejects.toThrow(
"No OpenSpec directory found. Run 'openspec init' first."
);
});
it('should handle configurator errors gracefully', async () => {
// Create CLAUDE.md file but make it read-only to cause an error
const claudePath = path.join(testDir, 'CLAUDE.md');
await fs.writeFile(claudePath, '<!-- OPENSPEC:START -->\nOld\n<!-- OPENSPEC:END -->');
await fs.chmod(claudePath, 0o444); // Read-only
const consoleSpy = vi.spyOn(console, 'log');
const errorSpy = vi.spyOn(console, 'error');
// Execute update command - should not throw
await updateCommand.execute(testDir);
// Should report the failure
expect(errorSpy).toHaveBeenCalled();
expect(consoleSpy).toHaveBeenCalledWith(
'Updated OpenSpec instructions (README.md)\nFailed to update: CLAUDE.md'
);
// Restore permissions for cleanup
await fs.chmod(claudePath, 0o644);
consoleSpy.mockRestore();
errorSpy.mockRestore();
});
});