Compare commits

...
Author SHA1 Message Date
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 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
31 changed files with 1714 additions and 267 deletions
@@ -1,26 +0,0 @@
# Implementation Tasks
## 1. Core Implementation
- [ ] 1.1 Create `src/core/list.ts` with list logic
- [ ] 1.1.1 Implement directory scanning (exclude archive/)
- [ ] 1.1.2 Implement task counting from tasks.md files
- [ ] 1.1.3 Format output as simple table
- [ ] 1.2 Add list command to CLI in `src/cli/index.ts`
- [ ] 1.2.1 Register `openspec list` command
- [ ] 1.2.2 Connect to list.ts implementation
## 2. Error Handling
- [ ] 2.1 Handle missing openspec/changes/ directory
- [ ] 2.2 Handle changes without tasks.md files
- [ ] 2.3 Handle empty changes directory
## 3. Testing
- [ ] 3.1 Add tests for list functionality
- [ ] 3.1.1 Test with multiple changes
- [ ] 3.1.2 Test with completed changes
- [ ] 3.1.3 Test with no changes
- [ ] 3.1.4 Test error conditions
## 4. Documentation
- [ ] 4.1 Update CLI help text with list command
- [ ] 4.2 Add list command to README if applicable
@@ -1,15 +0,0 @@
# Add @requirement Markers for Requirement Identification
## Why
Specs contain WHEN/THEN patterns that define system requirements, but extracting these programmatically requires brittle regex parsing that may miss edge cases or break with formatting changes.
## What Changes
- Define @requirement marker convention for identifying key requirements in specs
- Each marker includes a brief identifier (e.g., @requirement user-register)
- Markers appear directly before their WHEN/THEN blocks
- Document convention in openspec-conventions spec
## Impact
- Affected specs: openspec-conventions (new)
- Affected code: None initially - enables future tooling
- Breaking changes: None - additive convention only
@@ -1,24 +0,0 @@
# Implementation Tasks
## 1. Define Convention
- [ ] 1.1 Document @requirement marker syntax
- [ ] 1.2 Define identifier naming guidelines
- [ ] 1.3 Specify marker placement rules
- [ ] 1.4 Add examples of proper usage
## 2. Create Specification
- [ ] 2.1 Write openspec-conventions spec
- [ ] 2.2 Include requirement marker section
- [ ] 2.3 Add good and bad examples
- [ ] 2.4 Document edge cases
## 3. Update Existing Specs
- [ ] 3.1 Add @requirement markers to cli-init spec
- [ ] 3.2 Add @requirement markers to cli-update spec
- [ ] 3.3 Add @requirement markers to cli-view spec
- [ ] 3.4 Review and update any other existing specs
## 4. Documentation
- [ ] 4.1 Update README with marker convention
- [ ] 4.2 Add marker usage to CLAUDE.md
- [ ] 4.3 Create examples for AI assistants
@@ -0,0 +1,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
- [ ] 1.1 Update openspec-conventions spec with delta-based approach
- [ ] 1.2 Add Header-Based Requirement Identification
- [ ] 1.3 Define ADDED/MODIFIED/REMOVED/RENAMED sections
- [ ] 1.4 Document standard output symbols (+ ~ - →)
- [ ] 1.5 Update openspec/README.md with delta-based conventions
- [ ] 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,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,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
@@ -36,6 +36,60 @@ 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
### 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
@@ -110,105 +164,6 @@ A proposal is NOT required for:
- Adding tests for existing behavior
- Documentation clarifications
## Requirement Markers
### Marker Syntax
@requirement marker-syntax
WHEN writing a requirement in a spec
THEN prefix it with @requirement followed by a brief kebab-case identifier
AND place the marker on the line immediately before the WHEN statement
@requirement marker-identifier
WHEN choosing an identifier for @requirement
THEN use kebab-case (lowercase with hyphens)
AND keep it brief but descriptive (2-4 words)
AND ensure it's unique within the spec
@requirement marker-placement
WHEN adding @requirement markers to a spec
THEN place them in the ## Behavior or ## Behaviors section
AND ensure each WHEN/THEN block has exactly one marker
AND maintain a blank line after each THEN block for readability
### Examples
@requirement valid-marker-example
WHEN a spec includes properly formatted markers
THEN tools can extract and identify requirements programmatically
AND the spec remains human-readable
Example of correct usage:
```markdown
## Behavior
@requirement user-register
WHEN user registers with valid email
THEN create account and send confirmation
@requirement user-login
WHEN user logs in with correct credentials
THEN return JWT token with user data
@requirement invalid-credentials
WHEN user provides invalid credentials
THEN return 401 unauthorized error
```
@requirement invalid-marker-detection
WHEN a requirement lacks an @requirement marker
THEN tools should gracefully skip it
AND optionally warn about unmarked requirements
### Edge Cases
@requirement multiline-when-then
WHEN a WHEN or THEN clause spans multiple lines
THEN the @requirement marker still goes on the line before WHEN
AND the entire block is considered part of that requirement
@requirement multiple-then-clauses
WHEN a requirement has multiple THEN clauses using AND
THEN treat them as part of the same requirement
AND use a single @requirement marker for the entire block
@requirement nested-conditions
WHEN requirements have nested conditions or complex logic
THEN keep the @requirement marker simple
AND let the WHEN/THEN content contain the complexity
## Spec Structure
@requirement spec-file-location
WHEN creating a spec file
THEN place it in openspec/specs/[capability-name]/spec.md
AND use kebab-case for the capability name
@requirement spec-sections
WHEN structuring a spec
THEN include these sections in order:
- # [Capability Name] Specification
- ## Purpose (brief description)
- ## Behavior or ## Behaviors (with @requirement markers)
- ## Examples (optional, for complex requirements)
## Benefits of Requirement Markers
@requirement tooling-extraction
WHEN tools need to extract requirements from specs
THEN they can parse @requirement markers reliably
AND avoid complex regex patterns for WHEN/THEN extraction
@requirement requirement-counting
WHEN displaying change summaries
THEN tools can count requirements by counting @requirement markers
AND show accurate requirement counts per spec
@requirement requirement-referencing
WHEN documenting or discussing specific requirements
THEN use the @requirement identifier for clear reference
AND maintain consistency across documentation
## Why This Approach
Clean future state storage provides:
@@ -216,4 +171,10 @@ 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
- **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.
+46 -26
View File
@@ -6,45 +6,65 @@ 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.
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 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"
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 block in `CLAUDE.md` using markers
- **AND** use the default directory name `openspec`
- **AND** be idempotent (repeated runs have no additional effect)
## Edge Cases
### File Permissions
IF file write fails THEN let the error bubble up naturally with file path.
### 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 CLAUDE.md
- **WHEN** CLAUDE.md doesn't exist
- **THEN** create it with the template content
#### 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
+69 -12
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,63 @@ 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: Future State Storage
WHEN creating a change proposal
THEN store the complete future state of affected specs
AND use clean markdown without diff syntax
Change proposals SHALL store complete future state specifications without diff syntax.
#### Scenario: Creating change proposals
- **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
### Requirement: Proposal Format
WHEN documenting what changes
THEN the proposal SHALL explicitly describe each change:
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 +124,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 +169,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
+16
View File
@@ -6,6 +6,7 @@ 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();
@@ -90,4 +91,19 @@ program
}
});
program
.command('archive [change-name]')
.description('Archive a completed change and update main specs')
.option('-y, --yes', 'Skip confirmation prompts')
.action(async (changeName?: string, options?: { yes?: 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();
+224
View File
@@ -0,0 +1,224 @@
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 } = {}): 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.`);
}
}
// 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}`);
}
if (!options.yes) {
const proceed = await confirm({
message: 'Proceed with spec updates?',
default: true
});
if (!proceed) {
console.log('Archive cancelled.');
return;
}
}
// 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;
}
+269
View File
@@ -0,0 +1,269 @@
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);
});
});
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();
});
});
});