Compare commits

...
Author SHA1 Message Date
Tabish Bidiwale 82ba1f504e merge: resolve conflicts with main branch 2025-09-09 14:12:05 +10:00
Tabish Bidiwale d54fcc97f2 docs: mark completed tasks for diff command removal 2025-09-09 14:07:52 +10:00
Tabish Bidiwale ebff738860 feat: remove diff command in favor of show command
The diff command added unnecessary complexity and duplicated functionality
already available through the show command. Users can now use:
- `openspec show <change>` for structured change viewing
- `openspec show <change> --json --deltas-only` for delta-only views
- Standard git diff or other tools for file comparisons

This change:
- Removes ~227 lines of code and the jest-diff dependency
- Simplifies the CLI interface
- Reduces maintenance burden
- Aligns with verb-first command structure
2025-09-09 14:05:41 +10:00
Tabish Bidiwale 1bdaeef4da Update README.md 2025-09-07 09:16:53 +10:00
Tabish Bidiwale 2921676e93 docs(readme): make alignment the central value proposition 2025-09-07 05:16:40 +10:00
Tabish Bidiwale 792129bfe3 docs(readme): highlight supported AI tools and emphasize universal interoperability 2025-09-07 05:10:46 +10:00
Tabish Bidiwale 715ff513e3 docs(readme): restructure for clarity - focus on AI alignment benefits and quick wins 2025-09-07 05:07:21 +10:00
Tabish Bidiwale f6913b7661 docs(readme): streamline content, focus on change management vs Kiro 2025-09-07 04:53:19 +10:00
Tabish Bidiwale 730bbc00af docs(readme): remove JSON for automation section 2025-09-07 04:46:18 +10:00
Tabish Bidiwale adfcc65b9f docs(readme): simplify getting started with clearer AI workflow steps 2025-09-07 04:41:56 +10:00
Tabish Bidiwale 590541277d docs(readme): fix getting started to show AI-native workflow, not manual file creation 2025-09-07 04:36:03 +10:00
Tabish Bidiwale 14e2cc628a docs(readme): enhance for public release with why, workflow diagram, AI integration, comparisons 2025-09-07 04:26:44 +10:00
Tabish Bidiwale 299171c5cb docs(readme): add CI, npm, Node, license, conventional commits badges 2025-09-07 03:57:33 +10:00
Tabish Bidiwale bb6aae0205 docs(license): add MIT license file 2025-09-07 03:32:27 +10:00
Tabish Bidiwale 23c0ab6358 docs(readme): improve onboarding, verb-first commands, examples, JSON usage, troubleshooting 2025-09-07 03:20:34 +10:00
Tabish Bidiwale 02fe5b3547 Merge pull request #56 from Fission-AI/fix-tests
fix(test): resolve CI test failures with proper build setup
2025-09-07 02:34:13 +10:00
Tabish Bidiwale 57216a7824 refactor(test): use vitest globalSetup for build instead of per-test builds 2025-09-07 02:16:17 +10:00
Tabish Bidiwale 6e210cf084 fix(test): ensure dist exists before spawning CLI subprocesses 2025-09-07 02:13:21 +10:00
Tabish Bidiwale 5c6ae8e407 Merge pull request #55 from Fission-AI/fix-tests
fix(ci): ensure build runs before tests in workflows
2025-09-07 02:04:04 +10:00
Tabish Bidiwale 5376030421 fix(ci): simplify to single Node version for faster CI 2025-09-07 01:56:00 +10:00
Tabish Bidiwale 7d735eb2d8 fix(ci): ensure build runs before tests in workflows 2025-09-07 01:45:11 +10:00
Tabish Bidiwale 4d55e9ac6d chore(ci): use NODE_AUTH_TOKEN auth, add debug, build before tests 2025-09-07 01:23:32 +10:00
Tabish Bidiwale 9d674b22a3 Fix provenance 2025-09-07 01:13:23 +10:00
Tabish Bidiwale 96458ced1f Update actions workflow 2025-09-07 01:01:55 +10:00
Tabish Bidiwale 3d8f2a5974 update workflow 2025-09-07 00:31:37 +10:00
Tabish Bidiwale 006676c973 chore(test): clarify vitest worker note and newline 2025-09-06 23:27:49 +10:00
Tabish Bidiwale 63f45c0fcf test(commands): isolate change command tests via temp fixtures 2025-09-06 23:27:42 +10:00
Tabish Bidiwale 522126a6ee fix(utils): harden item discovery for determinism 2025-09-06 23:27:33 +10:00
Tabish Bidiwale 23b8030494 fix(change): only list active changes with proposal.md 2025-09-06 23:26:51 +10:00
Tabish Bidiwale f70df96656 docs(changes): add tasks.md for improve-deterministic-tests 2025-09-06 23:20:03 +10:00
Tabish Bidiwale df12368f11 chore(changes): remove obsolete cli-list spec 2025-09-06 21:01:19 +10:00
Tabish Bidiwale acd1ca28f3 docs(changes): add deterministic tests proposal 2025-09-06 21:01:19 +10:00
Tabish Bidiwale aedf4a34af docs(release): add 0.1.0 notes 2025-09-06 21:01:19 +10:00
Tabish Bidiwale f0c52ac7e8 Merge pull request #54 from Fission-AI/changeset-release/main
chore(release): version packages
2025-09-06 14:56:28 +10:00
github-actions[bot] f933e9b144 Version Packages 2025-09-06 04:43:55 +00:00
Tabish Bidiwale 24b4866426 chore(changeset): seed release notes 2025-09-06 14:35:50 +10:00
Tabish Bidiwale b7899602b4 chore(ci): add release workflows 2025-09-06 14:32:32 +10:00
Tabish Bidiwale b66d914198 chore(changesets): add config and script 2025-09-06 02:52:02 +10:00
Tabish Bidiwale 9926103505 chore(pkg): scope to @fission-ai and set public 2025-09-06 02:34:11 +10:00
Tabish Bidiwale 873e45a996 Merge pull request #53 from Fission-AI/prepare-publish
build: prepare for package publish
2025-09-06 02:28:48 +10:00
Tabish Bidiwale 573afa0c65 prepare for package publish 2025-09-06 02:22:38 +10:00
Tabish Bidiwale 665d740adb Remove retrospective doc 2025-09-01 11:34:23 +10:00
Tabish Bidiwale c35dd38567 Test cursor rules 2025-08-27 21:20:37 +10:00
Tabish Bidiwale aa9e49612b Merge pull request #51 from Fission-AI/updating-agent-instructions
feat: streamline OpenSpec agent instructions by 48%
2025-08-27 20:59:31 +10:00
Tabish Bidiwale 36eb0bbbf5 feat: streamline OpenSpec agent instructions by 48%
- Restructured README.md with three-stage workflow front-loaded
- Reduced from 575 to 298 lines while adding comprehensive content
- Added clear decision trees and removed ambiguous conditions
- Documented all CLI commands with examples and debugging tips
- Added critical scenario formatting guidance (most common error)
- Created troubleshooting section with error solutions
- Updated CLAUDE.md template with streamlined, focused content
- Added "Before Any Task" checklist for context gathering
- Added spec discovery workflow to prevent duplicates
- Included tool selection matrix and best practices
2025-08-27 18:22:29 +10:00
Tabish Bidiwale d549cec121 Merge pull request #50 from Fission-AI/update-openspec-agent-instructions
Update OpenSpec agent instructions for clarity and completeness
2025-08-27 18:01:29 +10:00
Tabish Bidiwale 1a3bfae784 feat: add comprehensive retrospective-based improvements
Based on OPENSPEC_COMPREHENSIVE_RETROSPECTIVE.md analysis:

Added critical missing documentation:
- Scenario formatting requirements (#### Scenario: headers) - #1 pain point
- Complete spec file structure examples with ADDED/MODIFIED sections
- Delta file location and extraction explanation
- Debugging commands (show --json --deltas-only)
- Troubleshooting section with common errors and solutions

Expanded implementation:
- Added 2 new task sections (Spec File Documentation, Troubleshooting)
- Increased from 41 to 52 total implementation tasks
- Added critical items to CLAUDE.md template tasks

This directly addresses the retrospective's top issues:
1. Scenario format documentation (marked "COMPLETELY MISSING")
2. Complete spec file examples
3. Delta detection debugging
4. Silent parsing failure explanations
2025-08-25 15:31:35 +10:00
Tabish Bidiwale fae08072a9 feat: add explicit implementation workflow for Stage 2
- Add detailed implementation steps: read docs → implement → mark complete
- Emphasize reading proposal.md, design.md, and tasks.md first
- Require immediate task completion marking (no batching)
- Add rationale: prevents jumping straight to code without context
- Update tasks to include implementation workflow documentation

This ensures agents understand and follow the complete change properly
2025-08-25 15:25:49 +10:00
Tabish Bidiwale 07df6c97c9 feat: add comprehensive CLI documentation and spec discovery workflow
- Document all 9 primary OpenSpec commands with examples
- Add openspec list and list --specs prominently
- Add "Before Creating Specs" rule to check existing specs first
- Document all CLI flags (--json, --type, --skip-specs, etc.)
- Update tasks to include 9 CLI documentation items
- Add spec discovery workflow to prevent duplicate capabilities

This ensures AI agents have complete CLI knowledge and avoid spec fragmentation
2025-08-25 15:21:09 +10:00
Tabish Bidiwale 7b13a2de03 feat: enhance proposal with agent instruction best practices
- Add decision clarity improvements (decision trees, remove ambiguity)
- Include agent-specific sections (tool selection, error recovery, context management)
- Restructure with clear information hierarchy
- Add comprehensive implementation tasks (6 sections, 26 tasks)
- Update design with industry best practices rationale

Based on analysis of Claude Code, Cursor, and other coding agent patterns
2025-08-24 13:36:12 +10:00
Tabish Bidiwale 41fc14d360 fix: remove spec deltas - this is a tooling change not a capability
- Documentation updates are tooling/infrastructure changes
- No specs needed for OpenSpec's own instructions
- Will use --skip-specs flag when archiving
2025-08-24 13:28:22 +10:00
Tabish Bidiwale 7c0face31b feat: add change proposal to update OpenSpec agent instructions
- Create proposal for streamlining agent instructions
- Document three-stage workflow clearly
- Update CLI command documentation
- Add best practices for AI agents
- Include spec deltas for documentation requirements
2025-08-24 13:25:23 +10:00
Tabish Bidiwale 332816cc35 Merge pull request #48 from Fission-AI/archive-changes
archive: apply delta-based spec updates and archive changes\n\n- adop…
2025-08-20 03:12:58 +10:00
Tabish Bidiwale 7ced2a8791 archive: apply delta-based spec updates and archive changes\n\n- adopt-delta-based-changes: fix MODIFIED/ADDED headers; update specs; archive\n- add-zod-validation: mark cli-diff validation as ADDED; archive\n- adopt-verb-noun-cli-structure: move Flags to MODIFIED; archive\n\nAlso adjust openspec-conventions deltas to reflect existing headers. 2025-08-20 03:11:00 +10:00
Tabish Bidiwale a79b8b5c03 Merge pull request #47 from Fission-AI/fix-invalid-spec-files
fix: fix invalid files
2025-08-20 01:42:09 +10:00
Tabish Bidiwale 52d620e40e fix invalid files 2025-08-20 01:41:36 +10:00
Tabish Bidiwale 22082338fd Merge pull request #46 from Fission-AI/feat/adopt-verb-noun-cli-structure
Adopt verb-noun CLI structure
2025-08-20 01:06:28 +10:00
Tabish Bidiwale 6458b6ed39 feat: adopt verb-noun CLI structure 2025-08-20 01:01:17 +10:00
Tabish Bidiwale 01a2f5d600 Merge pull request #45 from Fission-AI/feat/improve-validation-error-messages
feat(validate): improve error messages with actionable guidance
2025-08-20 01:00:14 +10:00
Tabish Bidiwale 95d855d641 feat(validate): improve error messages with actionable guidance 2025-08-20 00:54:34 +10:00
Tabish Bidiwale 562530dfa8 Merge pull request #44 from Fission-AI/feat/add-interactive-show-command
feat: add unified show command with interactive selection
2025-08-20 00:17:26 +10:00
Tabish Bidiwale 1e17cfdd0b Address review 2025-08-20 00:14:34 +10:00
Tabish Bidiwale 5d185ba3a8 feat: add unified show command with interactive selection 2025-08-20 00:06:19 +10:00
Tabish Bidiwale 08b41c7bea Merge pull request #43 from Fission-AI/feat/validate-command-interactive-selection
feat: add unified validate command with interactive selection and bulk operations
2025-08-19 23:23:11 +10:00
Tabish Bidiwale 8ac50289f0 Add tests 2025-08-19 23:22:18 +10:00
Tabish Bidiwale 1f295cec52 feat: add unified validate command with interactive selection and bulk operations 2025-08-19 22:58:17 +10:00
Tabish Bidiwale ad8e213cf9 Merge pull request #42 from Fission-AI/feat/bulk-validation-and-interactive-selection
feat: add bulk validation and interactive selection for OpenSpec commands
2025-08-19 22:35:25 +10:00
Tabish Bidiwale a6c1a90165 docs: consolidate retrospective documents into comprehensive analysis 2025-08-19 22:30:13 +10:00
Tabish Bidiwale 21b5a3e680 refactor: split validation and show commands into separate change proposals 2025-08-19 22:06:09 +10:00
Tabish Bidiwale 1bda5be96c refactor: use single validate command with flags for better UX 2025-08-19 21:40:27 +10:00
Tabish Bidiwale 0faf44807e refactor: simplify change to modify existing command specs instead of creating new ones 2025-08-19 21:29:38 +10:00
Tabish Bidiwale f9c1d07edb feat: add change proposal for bulk validation and interactive selection 2025-08-19 21:10:56 +10:00
Tabish Bidiwale 2bd1a4417c Merge pull request #41 from Fission-AI/chore/fix-change-validations
feat: Chore/fix change validations
2025-08-19 20:51:22 +10:00
108 changed files with 5977 additions and 1312 deletions
+6
View File
@@ -0,0 +1,6 @@
This directory is managed by Changesets.
- Add a changeset locally with `pnpm changeset`.
- The CI "Release (prepare)" workflow opens/updates a Version Packages PR.
- Publishing happens from a GitHub Release via the "Publish to npm" workflow.
+12
View File
@@ -0,0 +1,12 @@
{
"$schema": "https://unpkg.com/@changesets/config/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}
+302
View File
@@ -0,0 +1,302 @@
---
description: OpenSpec conventions and workflow for Cursor agents working in this repo
alwaysApply: false
---
# OpenSpec Rules for Cursor Agents
Instructions for AI coding assistants using OpenSpec for spec-driven development.
## Three-Stage Workflow
### Stage 1: Creating Changes
Create proposal when you need to:
- Add features or functionality
- Make breaking changes (API, schema)
- Change architecture or patterns
- Optimize performance (changes behavior)
- Update security patterns
Skip proposal for:
- Bug fixes (restore intended behavior)
- Typos, formatting, comments
- Dependency updates (non-breaking)
- Configuration changes
- Tests for existing behavior
### Stage 2: Implementing Changes
1. **Read proposal.md** - Understand what's being built
2. **Read design.md** (if exists) - Review technical decisions
3. **Read tasks.md** - Get implementation checklist
4. **Implement tasks sequentially** - Complete in order
5. **Mark complete immediately** - Update `- [x]` after each task
### Stage 3: Archiving Changes
After deployment, create separate PR to:
- Move `changes/[name]/` → `changes/archive/YYYY-MM-DD-[name]/`
- Update `specs/` if capabilities changed
- Use `openspec archive [change] --skip-specs` for tooling-only changes
## Before Any Task
**Context Checklist:**
- [ ] Read relevant specs in `specs/[capability]/spec.md`
- [ ] Check pending changes in `changes/` for conflicts
- [ ] Read `openspec/project.md` for conventions
- [ ] Run `openspec list` to see active changes
- [ ] Run `openspec list --specs` to see existing capabilities
**Before Creating Specs:**
- Always check if capability already exists
- Prefer modifying existing specs over creating duplicates
- Use `openspec show [spec]` to review current state
## Quick Start
### CLI Commands
```bash
# Essential commands
openspec list # List active changes
openspec list --specs # List specifications
openspec show [item] # Display change or spec
openspec diff [change] # Show spec differences
openspec validate [item] # Validate changes or specs
openspec archive [change] # Archive after deployment
# Project management
openspec init [path] # Initialize OpenSpec
openspec update [path] # Update instruction files
# Interactive mode
openspec show # Prompts for selection
openspec validate # Bulk validation mode
# Debugging
openspec show [change] --json --deltas-only
openspec validate [change] --strict
```
### Command Flags
- `--json` - Machine-readable output
- `--type change|spec` - Disambiguate items
- `--strict` - Comprehensive validation
- `--no-interactive` - Disable prompts
- `--skip-specs` - Archive without spec updates
## Directory Structure
```
openspec/
├── project.md # Project conventions
├── specs/ # Current truth - what IS built
│ └── [capability]/ # Single focused capability
│ ├── spec.md # Requirements and scenarios
│ └── design.md # Technical patterns
├── changes/ # Proposals - what SHOULD change
│ ├── [change-name]/
│ │ ├── proposal.md # Why, what, impact
│ │ ├── tasks.md # Implementation checklist
│ │ ├── design.md # Technical decisions (optional)
│ │ └── specs/ # Delta changes
│ │ └── [capability]/
│ │ └── spec.md # ADDED/MODIFIED/REMOVED
│ └── archive/ # Completed changes
```
## Creating Change Proposals
### Decision Tree
```
New request?
├─ Bug fix restoring spec behavior? → Fix directly
├─ Typo/format/comment? → Fix directly
├─ New feature/capability? → Create proposal
├─ Breaking change? → Create proposal
├─ Architecture change? → Create proposal
└─ Unclear? → Create proposal (safer)
```
### Proposal Structure
1. **Create directory:** `changes/[descriptive-name]/`
2. **Write proposal.md:**
```markdown
## Why
[1-2 sentences on problem/opportunity]
## What Changes
- [Bullet list of changes]
- [Mark breaking changes with **BREAKING**]
## Impact
- Affected specs: [list capabilities]
- Affected code: [key files/systems]
```
3. **Create spec deltas:** `specs/[capability]/spec.md`
```markdown
## ADDED Requirements
### Requirement: New Feature
The system SHALL provide...
#### Scenario: Success case
- **WHEN** user performs action
- **THEN** expected result
## MODIFIED Requirements
### Requirement: Existing Feature
[Complete modified requirement]
## REMOVED Requirements
### Requirement: Old Feature
**Reason**: [Why removing]
**Migration**: [How to handle]
```
4. **Create tasks.md:**
```markdown
## 1. Implementation
- [ ] 1.1 Create database schema
- [ ] 1.2 Implement API endpoint
- [ ] 1.3 Add frontend component
- [ ] 1.4 Write tests
```
## Spec File Format
### Critical: Scenario Formatting
**CORRECT** (use #### headers):
```markdown
#### Scenario: User login success
- **WHEN** valid credentials provided
- **THEN** return JWT token
```
**WRONG** (don't use bullets or bold):
```markdown
- **Scenario: User login** ❌
**Scenario**: User login ❌
### Scenario: User login ❌
```
Every requirement MUST have at least one scenario.
### Delta Operations
- `## ADDED Requirements` - New capabilities
- `## MODIFIED Requirements` - Changed behavior
- `## REMOVED Requirements` - Deprecated features
- `## RENAMED Requirements` - Name changes
Headers matched with `trim(header)` - whitespace ignored.
## Troubleshooting
### Common Errors
**"Change must have at least one delta"**
- Check `changes/[name]/specs/` exists with .md files
- Verify files have operation prefixes (## ADDED Requirements)
**"Requirement must have at least one scenario"**
- Check scenarios use `#### Scenario:` format (4 hashtags)
- Don't use bullet points or bold for scenario headers
**Silent scenario parsing failures**
- Exact format required: `#### Scenario: Name`
- Debug with: `openspec show [change] --json --deltas-only`
### Validation Tips
```bash
# Always use strict mode for comprehensive checks
openspec validate [change] --strict
# Debug delta parsing
openspec show [change] --json | jq '.deltas'
# Check specific requirement
openspec show [spec] --json -r 1
```
## Best Practices
### Simplicity First
- Default to <100 lines of new code
- Single-file implementations until proven insufficient
- Avoid frameworks without clear justification
- Choose boring, proven patterns
### Complexity Triggers
Only add complexity with:
- Performance data showing current solution too slow
- Concrete scale requirements (>1000 users, >100MB data)
- Multiple proven use cases requiring abstraction
### Clear References
- Use `file.ts:42` format for code locations
- Reference specs as `specs/auth/spec.md`
- Link related changes and PRs
### Capability Naming
- Use verb-noun: `user-auth`, `payment-capture`
- Single purpose per capability
- 10-minute understandability rule
- Split if description needs "AND"
## Tool Selection Guide
| Task | Tool | Why |
|------|------|-----|
| Find files by pattern | Glob | Fast pattern matching |
| Search code content | Grep | Optimized regex search |
| Read specific files | Read | Direct file access |
| Explore unknown scope | Task | Multi-step investigation |
## Error Recovery
### Change Conflicts
1. Run `openspec list` to see active changes
2. Check for overlapping specs
3. Coordinate with change owners
4. Consider combining proposals
### Validation Failures
1. Run with `--strict` flag
2. Check JSON output for details
3. Verify spec file format
4. Ensure scenarios properly formatted
### Missing Context
1. Read project.md first
2. Check related specs
3. Review recent archives
4. Ask for clarification
## Quick Reference
### Stage Indicators
- `changes/` - Proposed, not yet built
- `specs/` - Built and deployed
- `archive/` - Completed changes
### File Purposes
- `proposal.md` - Why and what
- `tasks.md` - Implementation steps
- `design.md` - Technical decisions
- `spec.md` - Requirements and behavior
### CLI Essentials
```bash
openspec list # What's in progress?
openspec show [item] # View details
openspec diff [change] # What's changing?
openspec validate --strict # Is it correct?
openspec archive [change] # Mark complete
```
Remember: Specs are truth. Changes are proposals. Keep them in sync.
+141
View File
@@ -0,0 +1,141 @@
name: CI
on:
pull_request:
branches: [main]
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
name: Test
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build project
run: pnpm run build
- name: Run tests
run: pnpm test
- name: Upload test coverage
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/
retention-days: 7
lint:
name: Lint & Type Check
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build project
run: pnpm run build
- name: Type check
run: pnpm exec tsc --noEmit
- name: Check for build artifacts
run: |
if [ ! -d "dist" ]; then
echo "Error: dist directory not found after build"
exit 1
fi
if [ ! -f "dist/cli/index.js" ]; then
echo "Error: CLI entry point not found"
exit 1
fi
validate-changesets:
name: Validate Changesets
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Validate changesets
run: |
if command -v changeset &> /dev/null; then
pnpm exec changeset status --since=origin/main
else
echo "Changesets not configured, skipping validation"
fi
required-checks:
name: All checks passed
runs-on: ubuntu-latest
needs: [test, lint]
if: always()
steps:
- name: Verify all checks passed
run: |
if [[ "${{ needs.test.result }}" != "success" ]]; then
echo "Test job failed"
exit 1
fi
if [[ "${{ needs.lint.result }}" != "success" ]]; then
echo "Lint job failed"
exit 1
fi
echo "All required checks passed!"
+37
View File
@@ -0,0 +1,37 @@
name: Release (prepare)
on:
push:
branches: [main]
permissions:
contents: write
pull-requests: write
jobs:
prepare:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
# Opens/updates the Version Packages PR; no publishing here
- name: Create/Update Version PR
uses: changesets/action@v1
with:
title: 'chore(release): version packages'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+72
View File
@@ -0,0 +1,72 @@
name: Publish to npm
on:
release:
types: [published]
workflow_dispatch: {}
permissions:
contents: read
id-token: write
concurrency:
group: publish-${{ github.ref }}
cancel-in-progress: false
jobs:
publish:
runs-on: ubuntu-latest
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
registry-url: 'https://registry.npmjs.org'
scope: '@fission-ai'
always-auth: true
- run: pnpm install --frozen-lockfile
- name: Build project
run: pnpm run build
- name: Ensure running from a tag
run: |
if [[ "$GITHUB_REF" != refs/tags/* ]]; then
echo "This workflow must run from a tag (got: $GITHUB_REF)";
exit 1;
fi
- name: Verify release tag matches package.json
run: |
TAG="${GITHUB_REF_NAME#v}"
PKG_VERSION=$(node -p "require('./package.json').version")
if [ "$TAG" != "$PKG_VERSION" ]; then
echo "Tag v$TAG does not match package.json $PKG_VERSION"; exit 1
fi
- name: Debug npm auth and context
run: |
test -n "$NODE_AUTH_TOKEN" || (echo "NODE_AUTH_TOKEN is missing" && exit 1)
echo "NODE_AUTH_TOKEN present"
npm --version
pnpm --version
node --version
npm config get registry
npm whoami
npm ping
- run: pnpm test
- name: Publish
run: pnpm publish --access public --provenance --no-git-checks --tag next
View File
+4
View File
@@ -0,0 +1,4 @@
### Minor Changes
- 24b4866: Initial release
+40
View File
@@ -0,0 +1,40 @@
<!-- OPENSPEC:START -->
# OpenSpec Project
This document provides instructions for AI coding assistants on how to use OpenSpec conventions for spec-driven development. Follow these rules precisely when working on OpenSpec-enabled projects.
This project uses OpenSpec for spec-driven development. Specifications are the source of truth.
See @openspec/AGENTS.md for detailed conventions and guidelines.
<!-- OPENSPEC:END -->
## Complexity Management
**Default to minimal solutions:**
- Propose <100 lines of new code for features
- Prefer single-file implementations until proven insufficient
- Avoid frameworks, abstractions, and optimizations without clear justification
- Choose boring, well-understood patterns over novel approaches
**Question requests for complexity:**
- Caching? → Ask for performance data and targets
- New framework? → Suggest plain code first
- Extra layers? → Start with the thinnest viable design
**Justify complexity with data:**
- Performance metrics showing current solution is too slow
- Concrete scale requirements (e.g., >1000 users, >100MB data)
- Multiple proven use cases requiring an abstraction
## Package Manager
Always use pnpm (NOT npm or yarn) for all Node.js package management:
- Install dependencies: `pnpm install`
- Add packages: `pnpm add [package]`
- Run scripts: `pnpm run [script]`
## Git Commits
Use conventional commits with these rules:
- Format: `type(scope): subject` (e.g., `fix: resolve auth error`, `feat(api): add user endpoint`)
- Keep commit messages to ONE line only - no body or footer
- Common types: feat, fix, docs, style, refactor, test, chore
- Never add co-authorship lines or attribution
+7
View File
@@ -0,0 +1,7 @@
# @fission-ai/openspec
## 0.1.0
### Minor Changes
- 24b4866: Initial release
+22
View File
@@ -0,0 +1,22 @@
MIT License
Copyright (c) 2024 OpenSpec Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+230 -79
View File
@@ -1,119 +1,270 @@
# OpenSpec
A specification-driven development system for maintaining living documentation alongside your code.
[![CI](https://github.com/Fission-AI/OpenSpec/actions/workflows/ci.yml/badge.svg)](https://github.com/Fission-AI/OpenSpec/actions/workflows/ci.yml)
[![npm version](https://img.shields.io/npm/v/@fission-ai/openspec)](https://www.npmjs.com/package/@fission-ai/openspec)
[![node](https://img.shields.io/node/v/@fission-ai/openspec)](https://nodejs.org/)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](./LICENSE)
[![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg)](https://conventionalcommits.org)
**Supported AI Tools:** ✅ Claude Code | 🔜 Cursor (coming soon) | 🔜 AGENTS.md support (coming soon)
Create **alignment** between humans and AI coding assistants through spec-driven development. **No API keys required.**
OpenSpec ensures you and your AI assistant agree on what to build before any code is written. By discussing and refining specifications first, you bring determinism to AI code generation, getting exactly what you want, not what the AI thinks you might want.
## Why OpenSpec?
**The Problem:** AI coding assistants are powerful but unpredictable. Without clear specifications, they generate code based on assumptions, often missing requirements or adding unwanted features. Teams waste time in review cycles because humans and AI aren't aligned on what to build.
**The Solution:** OpenSpec creates alignment BEFORE code is written:
- **Human-AI Alignment** - You and your AI agree on specifications before implementation
- **Deterministic Output** - Clear specs lead to predictable code generation
- **Team Alignment** - Everyone reviews specs, not code surprises
- **Focus on What, Not How** - Define requirements while AI handles implementation
- **Living Documentation** - Specs evolve with your code as a natural byproduct
## What You Get
- **Alignment First** - Ensure humans and AI agree on what to build before writing code
- **Predictable AI Output** - Turn non-deterministic AI into a reliable development partner
- **Universal Tool Support** - Works with any AI assistant - Claude Code, Cursor, or future tools
- **No API Keys Required** - Integrates through context rules, not external services
- **Spec-Level Reviews** - Teams review intentions, not implementation details
- **Clear Feature Scope** - Know exactly what you're building and what you're not
- **Progress Tracking** - See what's proposed, in progress, or completed at a glance
## How It Works
```
┌─────────────┐ ┌─────────────┐ ┌──────────────┐
│ SPECS │ │ CHANGES │ │ ARCHIVE │
│ (Truth) │◀──────│ (Proposals) │──────▶│ (Completed) │
└─────────────┘ └─────────────┘ └──────────────┘
▲ │ │
│ ▼ │
│ ┌─────────────┐ │
└───────────────│ CODE │◀──────────────┘
└─────────────┘
1. SPECS define current capabilities (what IS built)
2. CHANGES propose modifications using deltas (what SHOULD change)
3. CODE implements the changes following tasks
4. ARCHIVE preserves completed changes after deployment
```
## Installation
### Prerequisites
- Node.js >= 20.19.0
### Install OpenSpec
Install globally:
```bash
npm install -g openspec
npm install -g @fission-ai/openspec
```
## Quick Start
## Getting Started
### 1. Initialize OpenSpec in Your Project
```bash
# Initialize OpenSpec in your project
# Navigate to your project
cd my-project
# Initialize OpenSpec
openspec init
# Update existing OpenSpec instructions (team-friendly)
openspec update
# Select your AI tool (more coming soon!):
# "Which AI tool do you use?"
# > Claude Code
# Cursor (coming soon)
# List specs or changes
openspec spec list # specs (IDs by default; use --long for details)
openspec change list # changes (IDs by default; use --long for details)
# Show differences between specs and proposed changes
openspec diff [change-name]
# Archive completed changes
openspec archive [change-name]
# This creates:
# openspec/
# ├── specs/ # Current specifications (truth)
# ├── changes/ # Proposed changes
# └── README.md # AI instructions for your tool
```
## Commands
### 2. Create Your First Change
### `openspec init`
Jump straight into creating a change proposal with your AI assistant (works with Claude Code, Cursor, or any AI tool):
Initializes OpenSpec in your project by creating:
- `openspec/` directory structure
- `openspec/README.md` with OpenSpec instructions
- AI tool configuration files (based on your selection)
```markdown
// Quick win - Add a simple new feature:
You: "I want to add a user profile API endpoint.
Please create an OpenSpec change proposal for this."
### `openspec update`
AI: "I'll create an OpenSpec change proposal for the user profile API..."
*Creates openspec/changes/add-user-profile-api/ with:*
- proposal.md (why this feature is needed)
- tasks.md (implementation checklist)
- design.md (API design decisions)
- specs/user-profile/spec.md (new requirements)
Updates OpenSpec instructions to the latest version. This command is **team-friendly** and only updates files that already exist:
You: "The proposal looks good. Let's implement it."
- 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
AI: "Following the tasks in openspec/changes/add-user-profile-api/tasks.md:
Task 1.1: Create user profile model..."
*Implements each task systematically*
```
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.
### 3. Track Your Work
### `openspec spec`
```bash
# View active changes (what's being worked on)
openspec list
Manage and view specifications.
# Validate your changes are properly formatted
openspec validate add-2fa --strict
Examples:
- `openspec spec show <spec-id>`
- Text mode: prints raw `spec.md` content
- JSON mode (`--json`): returns minimal, stable shape
- Filters are JSON-only: `--requirements`, `--no-scenarios`, `-r/--requirement <1-based>`
- `openspec spec list`
- Prints IDs only by default
- Use `--long` to include `title` and `[requirements N]`
- `openspec spec validate <spec-id>`
- Text: human-readable summary to stdout/stderr
- `--json` for structured report
# After deployment, archive the completed change
openspec archive add-2fa
# This moves the change to archive/ and updates specs/
```
### `openspec change`
## Common Commands
Manage and view change proposals.
```bash
# Most used:
openspec list # See what changes you're working on
openspec archive <change> # Mark a change as complete after deployment
Examples:
- `openspec change show <change-id>`
- Text mode: prints raw `proposal.md` content
- JSON mode (`--json`): `{ id, title, deltaCount, deltas }`
- Filtering is JSON-only: `--deltas-only` (alias: `--requirements-only`, deprecated)
- `openspec change list`
- Prints IDs only by default
- Use `--long` to include `title` and counts `[deltas N] [tasks x/y]`
- `openspec change validate <change-id>`
- Text: human-readable result
- `--json` for structured report
# Also useful:
openspec validate <change> # Check formatting before committing
openspec show <change> # View change details
```
### `openspec diff [change-name]`
## Example: How AI Creates OpenSpec Files
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
When you ask your AI assistant to "add two-factor authentication", it creates:
### `openspec archive [change-name]`
```
openspec/
├── specs/
│ └── auth/
│ └── spec.md # Current auth spec (if exists)
└── changes/
└── add-2fa/ # AI creates this entire structure
├── proposal.md # Why and what changes
├── tasks.md # Implementation checklist
├── design.md # Technical decisions (optional)
└── specs/
└── auth/
└── spec.md # Delta showing additions
```
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)
### AI-Generated Spec (created in `openspec/specs/auth/spec.md`):
## Team Collaboration
```markdown
# Auth Specification
OpenSpec is designed for team collaboration:
## Purpose
Authentication and session management.
## Requirements
### Requirement: User Authentication
The system SHALL issue a JWT on successful login.
#### Scenario: Valid credentials
- WHEN a user submits valid credentials
- THEN a JWT is returned
```
### AI-Generated Change Delta (created in `openspec/changes/add-2fa/specs/auth/spec.md`):
```markdown
# Delta for Auth
## ADDED Requirements
### Requirement: Two-Factor Authentication
The system MUST require a second factor during login.
#### Scenario: OTP required
- WHEN a user submits valid credentials
- THEN an OTP challenge is required
```
### AI-Generated Tasks (created in `openspec/changes/add-2fa/tasks.md`):
```markdown
## 1. Database Setup
- [ ] 1.1 Add OTP secret column to users table
- [ ] 1.2 Create OTP verification logs table
## 2. Backend Implementation
- [ ] 2.1 Add OTP generation endpoint
- [ ] 2.2 Modify login flow to require OTP
- [ ] 2.3 Add OTP verification endpoint
## 3. Frontend Updates
- [ ] 3.1 Create OTP input component
- [ ] 3.2 Update login flow UI
```
**Important:** You don't create these files manually. Your AI assistant generates them based on your requirements and the existing codebase.
## Understanding OpenSpec Files
### Delta Format
Deltas are "patches" that show how specs change:
- **`## ADDED Requirements`** - New capabilities
- **`## MODIFIED Requirements`** - Changed behavior (include complete updated text)
- **`## REMOVED Requirements`** - Deprecated features
**Format requirements:**
- Use `### Requirement: <name>` for headers
- Every requirement needs at least one `#### Scenario:` block
- Use SHALL/MUST in requirement text
## Why OpenSpec Works
OpenSpec creates **alignment** between you and your AI coding assistant:
1. **You describe** what you want to build
2. **AI creates specs** before writing any code
3. **You review and adjust** the specifications
4. **AI implements** exactly what was specified
5. **Everyone understands** what's being built through clear specs
**True Interoperability:** OpenSpec is designed to be universal. No API keys, no vendor lock-in. It works by adding context rules to ANY AI coding tool - whether you use Claude Code today, switch to Cursor tomorrow, or adopt the next breakthrough AI assistant. Your specs remain portable and your workflow stays consistent.
## How OpenSpec Compares
### vs. Kiro.dev
OpenSpec groups all changes for a feature in one place (`openspec/changes/feature-name/`), making it easy to track what needs to be done. Kiro spreads changes across multiple spec folders, making feature tracking harder.
### vs. No Specs
Without specs, AI coding assistants generate code based on vague prompts, often missing requirements or adding unwanted features. OpenSpec ensures alignment before any code is written.
## Team Adoption
### Getting Started with Your Team
1. **Initialize OpenSpec** - Run `openspec init` in your project
2. **Start with new features** - Use OpenSpec for your next change proposal
3. **Build incrementally** - Each new feature adds to your spec library
4. **Future capability** - We're working on tools to generate specs from existing code
**Tool Freedom:** Your team can use different AI assistants. One developer might use Claude Code while another uses Cursor - OpenSpec keeps everyone aligned through shared specifications. Run `openspec update` to configure for any supported tool without affecting others.
1. **AI Tool Flexibility**: Each team member can use their preferred AI assistant (Claude, Cursor, etc.)
2. **Non-Invasive Updates**: The `update` command only modifies existing files, never forcing tools on team members
3. **Specification Sharing**: The `openspec/` directory contains shared specifications that all team members work from
4. **Change Tracking**: Proposed changes are visible to all team members for review before implementation
## Contributing
See `openspec/specs/` for the current system specifications and `openspec/changes/` for pending improvements.
## Notes
- The legacy `openspec list` command is deprecated. Use `openspec spec list` and `openspec change list`.
- Text output is raw-first (no formatting or filtering). Prefer `--json` for tooling-friendly output.
- Global `--no-color` disables ANSI colors and respects `NO_COLOR`.
- Install dependencies: `npm install`
- Build: `npm run build`
- Test: `npm test`
- Develop CLI locally: `npm run dev` or `npm run dev:cli`
- Conventional commits (one-line): `type(scope): subject`
## License
MIT
MIT
+3 -2
View File
@@ -11,10 +11,11 @@ if (existsSync('dist')) {
rmSync('dist', { recursive: true, force: true });
}
// Run TypeScript compiler
// Run TypeScript compiler (use local version explicitly)
console.log('Compiling TypeScript...');
try {
execSync('tsc', { stdio: 'inherit' });
execSync('./node_modules/.bin/tsc -v', { stdio: 'inherit' });
execSync('./node_modules/.bin/tsc', { stdio: 'inherit' });
console.log('\n✅ Build completed successfully!');
} catch (error) {
console.error('\n❌ Build failed!');
+233 -454
View File
@@ -1,517 +1,296 @@
# OpenSpec Instructions
This document provides instructions for AI coding assistants on how to use OpenSpec conventions for spec-driven development. Follow these rules precisely when working on OpenSpec-enabled projects.
Instructions for AI coding assistants using OpenSpec for spec-driven development.
## Core Principle
## Three-Stage Workflow
OpenSpec is an AI-native system for change-driven development where:
- **Specs** (`specs/`) reflect what IS currently built and deployed
- **Changes** (`changes/`) contain proposals for what SHOULD be changed
- **AI drives the process** - You generate proposals, humans review and approve
- **Specs are living documentation** - Always kept in sync with deployed code
### Stage 1: Creating Changes
Create proposal when you need to:
- Add features or functionality
- Make breaking changes (API, schema)
- Change architecture or patterns
- Optimize performance (changes behavior)
- Update security patterns
## Start Simple
Skip proposal for:
- Bug fixes (restore intended behavior)
- Typos, formatting, comments
- Dependency updates (non-breaking)
- Configuration changes
- Tests for existing behavior
**Default to minimal implementations:**
- New features should be <100 lines of code initially
- Use the simplest solution that works
- Avoid premature optimization (no caching, parallelization, or complex patterns without proven need)
- Choose boring technology over cutting-edge solutions
### Stage 2: Implementing Changes
1. **Read proposal.md** - Understand what's being built
2. **Read design.md** (if exists) - Review technical decisions
3. **Read tasks.md** - Get implementation checklist
4. **Implement tasks sequentially** - Complete in order
5. **Mark complete immediately** - Update `- [x]` after each task
**Complexity triggers** - Only add complexity when you have:
- **Performance data** showing current solution is too slow
- **Scale requirements** with specific numbers (>1000 users, >100MB data)
- **Multiple use cases** requiring the same abstraction
- **Regulatory compliance** mandating specific patterns
- **Security threats** that simple solutions cannot address
### Stage 3: Archiving Changes
After deployment, create separate PR to:
- Move `changes/[name]/` → `changes/archive/YYYY-MM-DD-[name]/`
- Update `specs/` if capabilities changed
- Use `openspec archive [change] --skip-specs` for tooling-only changes
When triggered, document the specific justification in your change proposal.
## Before Any Task
**Context Checklist:**
- [ ] Read relevant specs in `specs/[capability]/spec.md`
- [ ] Check pending changes in `changes/` for conflicts
- [ ] Read `openspec/project.md` for conventions
- [ ] Run `openspec list` to see active changes
- [ ] Run `openspec list --specs` to see existing capabilities
**Before Creating Specs:**
- Always check if capability already exists
- Prefer modifying existing specs over creating duplicates
- Use `openspec show [spec]` to review current state
## Quick Start
### CLI Commands
```bash
# Essential commands
openspec list # List active changes
openspec list --specs # List specifications
openspec show [item] # Display change or spec
openspec validate [item] # Validate changes or specs
openspec archive [change] # Archive after deployment
# Project management
openspec init [path] # Initialize OpenSpec
openspec update [path] # Update instruction files
# Interactive mode
openspec show # Prompts for selection
openspec validate # Bulk validation mode
# Debugging
openspec show [change] --json --deltas-only
openspec validate [change] --strict
```
### Command Flags
- `--json` - Machine-readable output
- `--type change|spec` - Disambiguate items
- `--strict` - Comprehensive validation
- `--no-interactive` - Disable prompts
- `--skip-specs` - Archive without spec updates
## Directory Structure
```
openspec/
├── project.md # Project-specific context (tech stack, conventions)
├── README.md # This file - OpenSpec instructions
├── project.md # Project conventions
├── specs/ # Current truth - what IS built
│ ├── [capability]/ # Single, focused capability
│ │ ├── spec.md # WHAT the capability does and WHY
│ │ └── design.md # HOW it's built (established patterns)
│ └── ...
├── changes/ # Proposed changes - what we're CHANGING
│ └── [capability]/ # Single focused capability
│ ├── spec.md # Requirements and scenarios
│ └── design.md # Technical patterns
├── changes/ # Proposals - what SHOULD change
│ ├── [change-name]/
│ │ ├── proposal.md # Why, what, impact (consolidated)
│ │ ├── proposal.md # Why, what, impact
│ │ ├── tasks.md # Implementation checklist
│ │ ├── design.md # Technical decisions (optional, for complex changes)
│ │ └── specs/ # Delta changes to specs
│ │ ├── design.md # Technical decisions (optional)
│ │ └── specs/ # Delta changes
│ │ └── [capability]/
│ │ └── spec.md # Delta format (ADDED/MODIFIED/REMOVED/RENAMED)
│ └── archive/ # Completed changes (dated)
│ │ └── spec.md # ADDED/MODIFIED/REMOVED
│ └── archive/ # Completed changes
```
### Capability Organization
## Creating Change Proposals
**Use capabilities, not features** - Each directory under `specs/` represents a single, focused responsibility:
- **Verb-noun naming**: `user-auth`, `payment-capture`, `order-checkout`
- **10-minute rule**: Each capability should be understandable in <10 minutes
- **Single purpose**: If it needs "AND" to describe it, split it
### Decision Tree
Examples:
```
✅ GOOD: user-auth, user-sessions, payment-capture, payment-refunds
❌ BAD: users, payments, core, misc
New request?
├─ Bug fix restoring spec behavior? → Fix directly
├─ Typo/format/comment? → Fix directly
├─ New feature/capability? → Create proposal
├─ Breaking change? → Create proposal
├─ Architecture change? → Create proposal
└─ Unclear? → Create proposal (safer)
```
## Key Behavioral Rules
### Proposal Structure
### 1. Always Start by Reading
1. **Create directory:** `changes/[descriptive-name]/`
Before any task:
1. **Read relevant specs** in `specs/[capability]/spec.md` to understand current state
2. **Check pending changes** in `changes/` directory for potential conflicts
3. **Read project.md** for project-specific conventions
2. **Write proposal.md:**
```markdown
## Why
[1-2 sentences on problem/opportunity]
### 2. When to Create Change Proposals
## What Changes
- [Bullet list of changes]
- [Mark breaking changes with **BREAKING**]
**ALWAYS create a change proposal for:**
- New features or functionality
- Breaking changes (API changes, schema updates)
- Architecture changes or new patterns
- Performance optimizations that change behavior
- Security updates affecting auth/access patterns
- Any change requiring multiple steps or affecting multiple systems
**SKIP proposals for:**
- Bug fixes that restore intended behavior
- Typos, formatting, or comment updates
- Dependency updates (unless breaking)
- Configuration or environment variable changes
- Adding tests for existing behavior
- Documentation fixes
**Complexity assessment:**
- If your solution requires >100 lines of new code, justify the complexity
- If adding dependencies, frameworks, or architectural patterns, document why simpler alternatives won't work
- Default to single-file implementations until proven insufficient
### 3. Delta-Based Change Format
Changes use a delta format with clear sections:
## Impact
- Affected specs: [list capabilities]
- Affected code: [key files/systems]
```
3. **Create spec deltas:** `specs/[capability]/spec.md`
```markdown
## ADDED Requirements
### Requirement: New Feature
[Complete requirement content in structured format]
The system SHALL provide...
## MODIFIED Requirements
#### Scenario: Success case
- **WHEN** user performs action
- **THEN** expected result
## MODIFIED Requirements
### Requirement: Existing Feature
[Complete modified requirement (header must match current spec)]
[Complete modified requirement]
## 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`
**Reason**: [Why removing]
**Migration**: [How to handle]
```
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. **Create tasks.md:**
```markdown
## 1. Implementation
- [ ] 1.1 Create database schema
- [ ] 1.2 Implement API endpoint
- [ ] 1.3 Add frontend component
- [ ] 1.4 Write tests
```
### 4. Creating a Change Proposal
## Spec File Format
When a user requests a significant change:
### Critical: Scenario Formatting
**CORRECT** (use #### headers):
```markdown
#### Scenario: User login success
- **WHEN** valid credentials provided
- **THEN** return JWT token
```
**WRONG** (don't use bullets or bold):
```markdown
- **Scenario: User login** ❌
**Scenario**: User login ❌
### Scenario: User login ❌
```
Every requirement MUST have at least one scenario.
### Delta Operations
- `## ADDED Requirements` - New capabilities
- `## MODIFIED Requirements` - Changed behavior
- `## REMOVED Requirements` - Deprecated features
- `## RENAMED Requirements` - Name changes
Headers matched with `trim(header)` - whitespace ignored.
## Troubleshooting
### Common Errors
**"Change must have at least one delta"**
- Check `changes/[name]/specs/` exists with .md files
- Verify files have operation prefixes (## ADDED Requirements)
**"Requirement must have at least one scenario"**
- Check scenarios use `#### Scenario:` format (4 hashtags)
- Don't use bullet points or bold for scenario headers
**Silent scenario parsing failures**
- Exact format required: `#### Scenario: Name`
- Debug with: `openspec show [change] --json --deltas-only`
### Validation Tips
```bash
# 1. Create the change directory
openspec/changes/[descriptive-name]/
# Always use strict mode for comprehensive checks
openspec validate [change] --strict
# 2. Generate proposal.md with all context
## Why
[1-2 sentences on the problem/opportunity]
# Debug delta parsing
openspec show [change] --json | jq '.deltas'
## What Changes
[Bullet list of changes, including breaking changes]
## Impact
- Affected specs: [list capabilities that will change]
- Affected code: [list key files/systems]
# 3. Create delta specs for ALL affected capabilities
# - Store only the changes (not complete future state)
# - Use sections: ## ADDED, ## MODIFIED, ## REMOVED, ## RENAMED
# - Include complete requirements in their final form
# Example spec.md content:
# ## ADDED Requirements
# ### Requirement: Password Reset
# Users SHALL be able to reset passwords via email...
#
# ## MODIFIED Requirements
# ### Requirement: User Authentication
# [Complete modified requirement with new password reset hook]
specs/
└── [capability]/
└── spec.md # Contains delta sections
# 4. Create tasks.md with implementation steps
## 1. [Task Group]
- [ ] 1.1 [Specific task]
- [ ] 1.2 [Specific task]
# 5. For complex changes, add design.md
[Technical decisions and trade-offs]
# Check specific requirement
openspec show [spec] --json -r 1
```
### 5. The Change Lifecycle
## Best Practices
1. **Propose** → Create change directory with delta-based documentation
2. **Review** → User reviews and approves the proposal
3. **Implement** → Follow the approved tasks.md (can be multiple PRs)
4. **Deploy** → User confirms deployment
5. **Update Specs** → Apply deltas to sync specs/ with new reality (IF the change affects system capabilities)
6. **Archive** → Move to `changes/archive/YYYY-MM-DD-[name]/`
### Simplicity First
- Default to <100 lines of new code
- Single-file implementations until proven insufficient
- Avoid frameworks without clear justification
- Choose boring, proven patterns
### 6. Implementing Changes
### Complexity Triggers
Only add complexity with:
- Performance data showing current solution too slow
- Concrete scale requirements (>1000 users, >100MB data)
- Multiple proven use cases requiring abstraction
When implementing an approved change:
1. Follow the tasks.md checklist exactly
2. **Mark completed tasks** in tasks.md as you finish them (e.g., `- [x] 1.1 Task completed`)
3. Ensure code matches the proposed behavior
4. Update any affected tests
5. **Keep change in `changes/` directory** - do NOT archive in implementation PR
### Clear References
- Use `file.ts:42` format for code locations
- Reference specs as `specs/auth/spec.md`
- Link related changes and PRs
**Multiple Implementation PRs:**
- Changes can be implemented across multiple PRs
- Each PR should update tasks.md to mark what was completed
- Different developers can work on different task groups
- Example: PR #1 completes tasks 1.1-1.3, PR #2 completes tasks 2.1-2.4
### Capability Naming
- Use verb-noun: `user-auth`, `payment-capture`
- Single purpose per capability
- 10-minute understandability rule
- Split if description needs "AND"
### 7. Updating Specs and Archiving After Deployment
## Tool Selection Guide
**Create a separate PR after deployment** that:
1. Moves change to `changes/archive/YYYY-MM-DD-[name]/`
2. Updates relevant files in `specs/` to reflect new reality (if needed)
3. If design.md exists, incorporates proven patterns into `specs/[capability]/design.md`
| Task | Tool | Why |
|------|------|-----|
| Find files by pattern | Glob | Fast pattern matching |
| Search code content | Grep | Optimized regex search |
| Read specific files | Read | Direct file access |
| Explore unknown scope | Task | Multi-step investigation |
This ensures changes are only archived when truly complete and deployed.
## Error Recovery
### 8. Types of Changes That Don't Require Specs
### Change Conflicts
1. Run `openspec list` to see active changes
2. Check for overlapping specs
3. Coordinate with change owners
4. Consider combining proposals
Some changes only affect development infrastructure and don't need specs:
- Initial project setup (package.json, tsconfig.json, etc.)
- Development tooling changes (linters, formatters, build tools)
- CI/CD configuration
- Development dependencies
### Validation Failures
1. Run with `--strict` flag
2. Check JSON output for details
3. Verify spec file format
4. Ensure scenarios properly formatted
For these changes:
1. Implement → Deploy → Mark tasks complete → Archive
2. Skip the "Update Specs" step entirely
### Missing Context
1. Read project.md first
2. Check related specs
3. Review recent archives
4. Ask for clarification
### What Deserves a Spec?
## Quick Reference
Ask yourself:
- Is this a system capability that users or other systems interact with?
- Does it have ongoing behavior that needs documentation?
- Would a new developer need to understand this to work with the system?
### Stage Indicators
- `changes/` - Proposed, not yet built
- `specs/` - Built and deployed
- `archive/` - Completed changes
If NO to all → No spec needed (likely just tooling/infrastructure)
### File Purposes
- `proposal.md` - Why and what
- `tasks.md` - Implementation steps
- `design.md` - Technical decisions
- `spec.md` - Requirements and behavior
## Understanding Specs vs Code
### Specs Document WHAT and WHY
```markdown
# Authentication Spec
Users SHALL authenticate with email and password.
WHEN credentials are valid THEN issue JWT token.
WHEN credentials are invalid THEN return generic error.
WHY: Prevent user enumeration attacks.
### CLI Essentials
```bash
openspec list # What's in progress?
openspec show [item] # View details
openspec validate --strict # Is it correct?
openspec archive [change] # Mark complete
```
### Code Documents HOW
```javascript
// Implementation details
const user = await db.users.findOne({ email });
const valid = await bcrypt.compare(password, user.hashedPassword);
```
**Key Distinction**: Specs capture intent, constraints, and decisions that aren't obvious from code.
## Common Scenarios
### New Feature Request
```
User: "Add password reset functionality"
You should:
1. Read specs/user-auth/spec.md
2. Check changes/ for pending auth changes
3. Create changes/add-password-reset/ with:
- proposal.md describing the change
- specs/user-auth/spec.md with:
## ADDED Requirements
### Requirement: Password Reset
[Complete requirement for password reset]
## MODIFIED Requirements
### Requirement: User Authentication
[Updated to integrate with password reset]
4. Wait for approval before implementing
```
### Bug Fix
```
User: "Getting null pointer error when bio is empty"
You should:
1. Check if spec says bios are optional
2. If yes → Fix directly (it's a bug)
3. If no → Create change proposal (it's a behavior change)
```
### Infrastructure Setup
```
User: "Initialize TypeScript project"
You should:
1. Create change proposal for TypeScript setup
2. Implement configuration files (PR #1)
3. Mark tasks complete in tasks.md
4. After deployment, create separate PR to archive
(no specs update needed - this is tooling, not a capability)
```
## Summary Workflow
1. **Receive request** → Determine if it needs a change proposal
2. **Read current state** → Check specs and pending changes
3. **Create proposal** → Generate complete change documentation
4. **Get approval** → User reviews the proposal
5. **Implement** → Follow approved tasks, mark completed items in tasks.md
6. **Deploy** → User deploys the implementation
7. **Archive PR** → Create separate PR to:
- Move change to archive
- Update specs if needed
- Mark change as complete
## PR Workflow Examples
### Single Developer, Simple Change
```
PR #1: Implementation
- Implement all tasks
- Update tasks.md marking items complete
- Get merged and deployed
PR #2: Archive (after deployment)
- Move changes/feature-x/ → changes/archive/2025-01-15-feature-x/
- Update specs if needed
```
### Multiple Developers, Complex Change
```
PR #1: Alice implements auth components
- Complete tasks 1.1, 1.2, 1.3
- Update tasks.md marking these complete
PR #2: Bob implements UI components
- Complete tasks 2.1, 2.2
- Update tasks.md marking these complete
PR #3: Alice fixes integration issues
- Complete remaining task 1.4
- Update tasks.md
[Deploy all changes]
PR #4: Archive
- Move to archive with deployment date
- Update specs to reflect new auth flow
```
### Key Rules
- **Never archive in implementation PRs** - changes aren't done until deployed
- **Always update tasks.md** - shows accurate progress
- **One archive PR per change** - clear completion boundary
- **Archive PR includes spec updates** - keeps specs current
## Capability Organization Best Practices
### Naming Capabilities
- Use **verb-noun** patterns: `user-auth`, `payment-capture`, `order-checkout`
- Be specific: `payment-capture` not just `payments`
- Keep flat: Avoid nesting capabilities within capabilities
- Singular focus: If you need "AND" to describe it, split it
### When to Split Capabilities
Split when you have:
- Multiple unrelated API endpoints
- Different user personas or actors
- Separate deployment considerations
- Independent evolution paths
#### Capability Boundary Guidelines
- Would you import these separately? → Separate capabilities
- Different deployment cadence? → Separate capabilities
- Different teams own them? → Separate capabilities
- Shared data models are OK, shared business logic means combine
Examples:
- user-auth (login/logout) vs user-sessions (token management) → SEPARATE
- payment-capture vs payment-refunds → SEPARATE (different workflows)
- user-profile vs user-settings → COMBINE (same data model, same owner)
### Cross-Cutting Concerns
For system-wide policies (rate limiting, error handling, security), document them in:
- `project.md` for project-wide conventions
- Within relevant capability specs where they apply
- Or create a dedicated capability if complex enough (e.g., `api-rate-limiting/`)
### Examples of Well-Organized Capabilities
```
specs/
├── user-auth/ # Login, logout, password reset
├── user-sessions/ # Token management, refresh
├── user-profile/ # Profile CRUD operations
├── payment-capture/ # Processing payments
├── payment-refunds/ # Handling refunds
└── order-checkout/ # Checkout workflow
```
For detailed guidance, see the [Capability Organization Guide](../docs/capability-organization.md).
## Common Scenarios and Clarifications
### Decision Ambiguity: Bug vs Behavior Change
When specs are missing or ambiguous:
- If NO spec exists → Treat current code behavior as implicit spec, require proposal
- If spec is VAGUE → Require proposal to clarify spec alongside fix
- If code and spec DISAGREE → Spec is truth, code is buggy (fix without proposal)
- If unsure → Default to creating a proposal (safer option)
Example:
```
User: "The API returns 404 for missing users but should return 400"
AI: Is this a bug (spec says 400) or behavior change (spec says 404)?
```
### When You Don't Know the Scope
It's OK to explore first! Tell the user you need to investigate, then create an informed proposal.
### Exploration Phase (When Needed)
BEFORE creating proposal, you may need exploration when:
- User request is vague or high-level
- Multiple implementation approaches exist
- Scope is unclear without seeing code
Exploration checklist:
1. Tell user you need to explore first
2. Use Grep/Read to understand current state
3. Create initial proposal based on findings
4. Refine with user feedback
Example:
```
User: "Add caching to improve performance"
AI: "Let me explore the codebase to understand the current architecture and identify caching opportunities."
[After exploration]
AI: "Based on my analysis, I've identified three areas where caching would help. Here's my proposal..."
```
### When No Specs Exist
Treat current code as implicit spec. Your proposal should document current state AND proposed changes.
### When in Doubt
Default to creating a proposal. It's easier to skip an unnecessary proposal than fix an undocumented change.
### AI Workflow Adaptations
Task tracking with OpenSpec:
- Track exploration tasks separately from implementation
- Document proposal creation steps as you go
- Keep implementation tasks separate until proposal approved
Parallel operations encouraged:
- Read multiple specs simultaneously
- Check multiple pending changes at once
- Batch related searches for efficiency
Progress communication:
- "Exploring codebase to understand scope..."
- "Creating proposal based on findings..."
- "Implementing approved changes..."
### For AI Assistants
- **Bias toward simplicity** - Propose the minimal solution that works
- Use your exploration tools liberally before proposing
- Batch operations for efficiency
- Communicate your progress
- It's OK to revise proposals based on discoveries
- **Question complexity** - If your solution feels complex, simplify first
## Edge Case Handling
### Multi-Capability Changes
Create ONE proposal that:
- Lists all affected capabilities
- Shows changes per capability
- Has unified task list
- Gets approved as a whole
### Outdated Specs
If specs clearly outdated:
1. Create proposal to update specs to match reality
2. Implement new feature in separate proposal
3. OR combine both in one proposal with clear sections
### Emergency Hotfixes
For critical production issues:
1. Announce: "This is an emergency fix"
2. Implement fix immediately
3. Create retroactive proposal
4. Update specs after deployment
5. Tag with [EMERGENCY] in archive
### Pure Refactoring
No proposal needed for:
- Code formatting/style
- Internal refactoring (same API)
- Performance optimization (same behavior)
- Adding types to untyped code
Proposal REQUIRED for:
- API changes (even if compatible)
- Database schema changes
- Architecture changes
- New dependencies
### Observability Additions
No proposal needed for:
- Adding log statements
- New metrics/traces
- Debugging additions
- Error tracking
Proposal REQUIRED if:
- Changes log format/structure
- Adds new monitoring service
- Changes what's logged (privacy)
## Remember
- You are the process driver - automate documentation burden
- Specs must always reflect deployed reality
- Changes are proposed, not imposed
- Impact analysis prevents surprises
- Simplicity is the power - just markdown files, minimal solutions
- Start simple, add complexity only when justified
By following these conventions, you enable true spec-driven development where documentation stays current, changes are traceable, and evolution is intentional.
Remember: Specs are truth. Changes are proposals. Keep them in sync.
@@ -1,6 +1,6 @@
## MODIFIED Requirements
### Requirement: List Command Behavior
### Requirement: Command Execution
The current `list` command behavior SHALL be preserved but marked as deprecated.
@@ -0,0 +1,20 @@
## Why
Users frequently need to view changes and specs but must know in advance whether they're looking at a change or spec. The current subcommand structure (`change show`, `spec show`) creates friction when:
- Users want to quickly view an item without remembering its type
- Exploring the codebase requires switching between different show commands
- Show commands without arguments return errors instead of helpful guidance
## What Changes
- Add new top-level `show` command for displaying changes or specs with intelligent selection
- Support direct item display: `openspec show <item>` with automatic type detection
- Interactive selection when no arguments provided
- Enhance existing `change show` and `spec show` to support interactive selection (backwards compatibility)
- Maintain all existing format options (--json, --deltas-only, --requirements, etc.)
## Impact
- New specs to create: cli-show
- Specs to enhance: cli-change, cli-spec (for backwards compatibility)
- Affected code: src/cli/index.ts, src/commands/show.ts (new), src/commands/spec.ts, src/commands/change.ts
@@ -0,0 +1,23 @@
# CLI Change Command Spec
## ADDED Requirements
### Requirement: Interactive show selection
The change show command SHALL support interactive selection when no change name is provided.
#### Scenario: Interactive change selection for show
- **WHEN** executing `openspec change show` without arguments
- **THEN** display an interactive list of available changes
- **AND** allow the user to select a change to show
- **AND** display the selected change content
- **AND** maintain all existing show options (--json, --deltas-only)
#### Scenario: Non-interactive fallback keeps current behavior
- **GIVEN** stdin is not a TTY or `--no-interactive` is provided or environment variable `OPEN_SPEC_INTERACTIVE=0`
- **WHEN** executing `openspec change show` without a change name
- **THEN** do not prompt interactively
- **AND** print the existing hint including available change IDs
- **AND** set `process.exitCode = 1`
@@ -0,0 +1,83 @@
# CLI Show Command Spec
## ADDED Requirements
### Requirement: Top-level show command
The CLI SHALL provide a top-level `show` command for displaying changes and specs with intelligent selection.
#### Scenario: Interactive show selection
- **WHEN** executing `openspec show` without arguments
- **THEN** prompt user to select type (change or spec)
- **AND** display list of available items for selected type
- **AND** show the selected item's content
#### Scenario: Non-interactive environments do not prompt
- **GIVEN** stdin is not a TTY or `--no-interactive` is provided or environment variable `OPEN_SPEC_INTERACTIVE=0`
- **WHEN** executing `openspec show` without arguments
- **THEN** do not prompt
- **AND** print a helpful hint with examples for `openspec show <item>` or `openspec change/spec show`
- **AND** exit with code 1
#### Scenario: Direct item display
- **WHEN** executing `openspec show <item-name>`
- **THEN** automatically detect if item is a change or spec
- **AND** display the item's content
- **AND** use appropriate formatting based on item type
#### Scenario: Type detection and ambiguity handling
- **WHEN** executing `openspec show <item-name>`
- **THEN** if `<item-name>` uniquely matches a change or a spec, show that item
- **AND** if it matches both, print an ambiguity error and suggest `--type change|spec` or using `openspec change show`/`openspec spec show`
- **AND** if it matches neither, print not-found with nearest-match suggestions
#### Scenario: Explicit type override
- **WHEN** executing `openspec show --type change <item>`
- **THEN** treat `<item>` as a change ID and show it (skipping auto-detection)
- **WHEN** executing `openspec show --type spec <item>`
- **THEN** treat `<item>` as a spec ID and show it (skipping auto-detection)
### Requirement: Output format options
The show command SHALL support various output formats consistent with existing commands.
#### Scenario: JSON output
- **WHEN** executing `openspec show <item> --json`
- **THEN** output the item in JSON format
- **AND** include parsed metadata and structure
- **AND** maintain format consistency with existing change/spec show commands
#### Scenario: Flag scoping and delegation
- **WHEN** showing a change or a spec via the top-level command
- **THEN** accept common flags such as `--json`
- **AND** pass through type-specific flags to the corresponding implementation
- Change-only flags: `--deltas-only` (alias `--requirements-only` deprecated)
- Spec-only flags: `--requirements`, `--no-scenarios`, `-r/--requirement`
- **AND** ignore irrelevant flags for the detected type with a warning
### Requirement: Interactivity controls
- The CLI SHALL respect `--no-interactive` to disable prompts.
- The CLI SHALL respect `OPEN_SPEC_INTERACTIVE=0` to disable prompts globally.
- Interactive prompts SHALL only be shown when stdin is a TTY and interactivity is not disabled.
#### Scenario: Change-specific options
- **WHEN** showing a change with `openspec show <change-name> --deltas-only`
- **THEN** display only the deltas in JSON format
- **AND** maintain compatibility with existing change show options
#### Scenario: Spec-specific options
- **WHEN** showing a spec with `openspec show <spec-id> --requirements`
- **THEN** display only requirements in JSON format
- **AND** support other spec options (--no-scenarios, -r)
- **AND** maintain compatibility with existing spec show options
@@ -0,0 +1,23 @@
# CLI Spec Command Spec
## ADDED Requirements
### Requirement: Interactive spec show
The spec show command SHALL support interactive selection when no spec-id is provided.
#### Scenario: Interactive spec selection for show
- **WHEN** executing `openspec spec show` without arguments
- **THEN** display an interactive list of available specs
- **AND** allow the user to select a spec to show
- **AND** display the selected spec content
- **AND** maintain all existing show options (--json, --requirements, --no-scenarios, -r)
#### Scenario: Non-interactive fallback keeps current behavior
- **GIVEN** stdin is not a TTY or `--no-interactive` is provided or environment variable `OPEN_SPEC_INTERACTIVE=0`
- **WHEN** executing `openspec spec show` without a spec-id
- **THEN** do not prompt interactively
- **AND** print the existing error message for missing spec-id
- **AND** set non-zero exit code
@@ -0,0 +1,142 @@
# Implementation Tasks — Add Interactive Show Command
## Goals
- Add a top-level `show` command with intelligent selection and type detection.
- Add interactive selection to `change show` and `spec show` when no ID is provided.
- Preserve raw-first output behavior and existing JSON formats/filters.
- Respect `--no-interactive` and `OPEN_SPEC_INTERACTIVE=0` consistently.
---
## 1) CLI wiring
- [x] In `src/cli/index.ts` add a top-level command: `program.command('show [item-name]')`
- Options:
- `--json`
- `--type <type>` where `<type>` is `change|spec`
- `--no-interactive`
- Allow passing-through type-specific flags using `.allowUnknownOption(true)` so the top-level can forward flags to the underlying type handler.
- Action: instantiate `new ShowCommand().execute(itemName, options)`.
- [x] Update `change show` subcommand to accept `--no-interactive` and pass it to `ChangeCommand.show(...)`.
- [x] Change `spec show` subcommand to accept optional ID (`show [spec-id]`), add `--no-interactive`, and pass to spec show implementation.
Acceptance:
- `openspec show` exists and prints a helpful hint in non-interactive contexts when no args.
- Unknown flags for other types do not crash parsing; they are warned/ignored appropriately.
---
## 2) New module: `src/commands/show.ts`
- [x] Create `ShowCommand` with:
- `execute(itemName?: string, options?: { json?: boolean; type?: string; noInteractive?: boolean; [k: string]: any })`
- Interactive path when `!itemName` and interactive is enabled:
- Prompt: "What would you like to show?" → `change` or `spec`.
- Load available IDs for the chosen type and prompt selection.
- Delegate to type-specific show implementation.
- Non-interactive path when `!itemName`:
- Print hint with examples:
- `openspec show <item>`
- `openspec change show`
- `openspec spec show`
- Exit with code 1.
- Direct item path when `itemName` is provided:
- Type override via `--type` takes precedence.
- Otherwise detect using `getActiveChangeIds()` and `getSpecIds()`.
- If ambiguous and no override: print error + suggestion to pass `--type` or use subcommands; exit code 1.
- If unknown: print not-found with nearest-match suggestions; exit code 1.
- On success: delegate to type-specific show.
- [x] Flag scoping and pass-through:
- Common: `--json` → forwarded to both types.
- Change-only: `--deltas-only`, `--requirements-only` (deprecated alias).
- Spec-only: `--requirements`, `--no-scenarios`, `-r/--requirement`.
- Warn and ignore irrelevant flags for the resolved type.
Acceptance:
- `openspec show <change-id> --json --deltas-only` matches `openspec change show <id> --json --deltas-only` output.
- `openspec show <spec-id> --json --requirements` matches `openspec spec show <id> --json --requirements` output.
- Ambiguity and not-found behaviors match the `cli-show` spec.
---
## 3) Refactor spec show into reusable API
- [x] In `src/commands/spec.ts`, extract show logic into an exported `SpecCommand` with `show(specId?: string, options?: { json?: boolean; requirements?: boolean; scenarios?: boolean; requirement?: string; noInteractive?: boolean })`.
- Reuse current helpers (`parseSpecFromFile`, `filterSpec`, raw-first printing).
- Keep `registerSpecCommand` but delegate to `new SpecCommand().show(...)`.
- [x] Update CLI spec show subcommand to optional arg and interactive behavior (see section 4).
Acceptance:
- Existing `spec show` tests continue to pass.
- New `SpecCommand.show` can be called from `ShowCommand`.
---
## 4) Backwards-compatible interactive in subcommands
- [x] `src/commands/change.ts` → extend `show(changeName?: string, options?: { json?: boolean; requirementsOnly?: boolean; deltasOnly?: boolean; noInteractive?: boolean })`:
- When `!changeName` and interactive enabled: prompt from `getActiveChangeIds()` and show the selected change.
- Non-interactive fallback: keep current behavior (print available IDs + `openspec change list` hint, set `process.exitCode = 1`).
- [x] `src/commands/spec.ts` → `SpecCommand.show` as above:
- When `!specId` and interactive enabled: prompt from `getSpecIds()` and show the selected spec.
- Non-interactive fallback: print the same error as existing behavior for missing `<spec-id>` and set non-zero exit code.
Acceptance:
- `openspec change show` in non-interactive prints list hint and exits non-zero.
- `openspec spec show` in non-interactive prints missing-arg error and exits non-zero.
---
## 5) Shared utilities
- [x] Extract `nearestMatches` and `levenshtein` from `src/commands/validate.ts` into `src/utils/match.ts` (exported helpers).
- [x] Update `ValidateCommand` and new `ShowCommand` to import from `utils/match`.
Acceptance:
- Build succeeds with shared helpers and no duplication.
---
## 6) Hints, warnings, and messages
- [x] Top-level `show` hint (non-interactive no-arg):
- Lines include: `openspec show <item>`, `openspec change show`, `openspec spec show`, and "Or run in an interactive terminal.".
- [x] Ambiguity message suggests `--type change|spec` and the subcommands.
- [x] Not-found suggests nearest matches (up to 5).
- [x] Irrelevant flag warnings for the resolved type (printed to stderr, no crash).
Acceptance:
- Messages match the `cli-show` spec wording intent and style used elsewhere.
---
## 7) Tests
Add tests mirroring existing patterns (non-TTY simulation via `OPEN_SPEC_INTERACTIVE=0`).
- [x] `test/commands/show.test.ts`
- Non-interactive, no arg → prints hint and exits non-zero.
- Direct item detection for change and for spec.
- Ambiguity case when both exist → error and suggestion for `--type`.
- Not-found case → nearest-match suggestions.
- Pass-through flags: change `--json --deltas-only`, spec `--json --requirements`.
- [x] `test/commands/change.interactive-show.test.ts` (non-interactive fallback)
- Ensure `openspec change show` without args prints available IDs + list hint and non-zero exit.
- [x] `test/commands/spec.interactive-show.test.ts` (non-interactive fallback)
- Ensure `openspec spec show` without args prints missing-arg error and non-zero exit.
Acceptance:
- All new tests pass after build; no regressions in existing tests.
---
## 8) Documentation (optional but recommended)
- [x] Update `openspec/README.md` usage examples to include the new `show` command with type detection and flags.
---
## 9) Non-functional checks
- [x] Run `pnpm build` and all tests (`pnpm test`).
- [x] Ensure no linter/type errors and messages are consistent with existing style.
---
## Notes on consistency
- Follow raw-first behavior for text output: passthrough file content with no formatting, mirroring current `change show` and `spec show`.
- Reuse `isInteractive` and `item-discovery` helpers for consistent prompting behavior.
- Keep JSON output shapes identical to current `ChangeCommand.show` and `spec show` outputs.
@@ -1,4 +1,4 @@
## MODIFIED Requirements
## ADDED Requirements
### Requirement: Diff Command Enhancement
@@ -24,6 +24,8 @@ Before moving the change to archive, the command SHALL apply delta changes to ma
- **THEN** abort with error message showing the conflict
- **AND** suggest manual resolution
## ADDED Requirements
### Requirement: Display Output
The command SHALL provide clear feedback about delta operations.
@@ -31,6 +31,8 @@ The command SHALL show a requirement-level comparison displaying only changed re
- Indicates removed requirements (not in future)
- Aligns modified requirements for easy comparison
## ADDED Requirements
### Requirement: Validation
The command SHALL validate that changes can be applied successfully.
@@ -1,6 +1,6 @@
# OpenSpec Conventions - Changes
## ADDED Requirements
## MODIFIED Requirements
### Requirement: Header-Based Requirement Identification
@@ -31,8 +31,6 @@ Requirement headers SHALL serve as unique identifiers for programmatic matching
- **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.
@@ -100,18 +98,4 @@ The archive process SHALL programmatically apply delta changes to current specif
- **AND** require manual resolution before proceeding
- **AND** provide clear guidance on resolving conflicts
## REMOVED Requirements
### Requirement: Future State Storage
The system SHALL no longer store complete future-state specifications in change proposals.
**Reason for removal**: Replaced by delta-based change storage which provides better review experience and clearer change tracking.
**Migration path**: All new changes must use delta format.
#### Scenario: Deprecate future state storage
- **WHEN** creating a new change proposal
- **THEN** do not include full future-state specs
- **AND** include only ADDED/MODIFIED/REMOVED/RENAMED requirements under the change's `specs/` directory
@@ -0,0 +1,19 @@
# Design: Verb–Noun CLI Structure Adoption
## Overview
We will make verb commands (`list`, `show`, `validate`, `diff`, `archive`) the primary interface and keep noun commands (`spec`, `change`) as deprecated aliases for one release.
## Decisions
1. Keep routing centralized in `src/cli/index.ts`.
2. Add `--specs`/`--changes` to `openspec list`, with `--changes` as default.
3. Show deprecation warnings for `openspec change list` and, more generally, for any `openspec change ...` and `openspec spec ...` subcommands.
4. Do not change `show`/`validate` behavior beyond help text; they already support `--type` for disambiguation.
## Backward Compatibility
All noun-based commands continue to work with clear deprecation warnings directing users to verb-first equivalents.
## Out of Scope
JSON output parity for `openspec list` across modes and `show --specs/--changes` discovery are follow-ups.
@@ -0,0 +1,67 @@
# Change: Adopt Verb–Noun CLI Structure (Deprecate Noun-Based Commands)
## Why
Most widely used CLIs (git, docker, kubectl) start with an action (verb) followed by the object (noun). This matches how users think: “do X to Y”. Using verbs as top-level commands improves clarity, discoverability, and extensibility.
## What Changes
- Promote top-level verb commands as primary entry points: `list`, `show`, `validate`, `diff`, `archive`.
- Deprecate noun-based top-level commands: `openspec spec ...` and `openspec change ...`.
- Introduce consistent noun scoping via flags where applicable (e.g., `--changes`, `--specs`) and keep smart defaults.
- Clarify disambiguation for `show` and `validate` when names collide.
### Mappings (From → To)
- **List**
- From: `openspec change list`
- To: `openspec list --changes` (default), or `openspec list --specs`
- **Show**
- From: `openspec spec show <spec-id>` / `openspec change show <change-id>`
- To: `openspec show <item-id>` with auto-detect, use `--type spec|change` if ambiguous
- **Validate**
- From: `openspec spec validate <spec-id>` / `openspec change validate <change-id>`
- To: `openspec validate <item-id> --type spec|change`, or bulk: `openspec validate --specs` / `--changes` / `--all`
### Backward Compatibility
- Keep `openspec spec` and `openspec change` available with deprecation warnings for one release cycle.
- Update help text to point users to the verb–noun alternatives.
## Impact
- **Affected specs**:
- `cli-list`: Add support for `--specs` and explicit `--changes` (default remains changes)
- `openspec-conventions`: Add explicit requirement establishing verb–noun CLI design and deprecation guidance
- **Affected code**:
- `src/cli/index.ts`: Un-deprecate top-level `list`; mark `change list` as deprecated; ensure help text and warnings align
- `src/core/list.ts`: Support listing specs via `--specs` and default to changes; shared output shape
- Optional follow-ups: tighten `show`/`validate` help and ambiguity handling
## Explicit Changes
**CLI Design**
- From: Mixed model with nouns (`spec`, `change`) and some top-level verbs; `openspec list` currently deprecated
- To: Verbs as primary: `openspec list|show|validate|diff|archive`; nouns scoped via flags or item ids; noun commands deprecated
- Reason: Align with common CLIs; improve UX; simpler mental model
- Impact: Non-breaking with deprecation period; users migrate incrementally
**Listing Behavior**
- From: `openspec change list` (primary), `openspec list` (deprecated)
- To: `openspec list` as primary, defaulting to `--changes`; add `--specs` to list specs
- Reason: Consistent verb–noun style; better discoverability
- Impact: New option; preserves existing behavior via default
## Rollout and Deprecation Policy
- Show deprecation warnings on noun-based commands for one release.
- Document new usage in `openspec/README.md` and CLI help.
- After one release, consider removing noun-based commands, or keep as thin aliases without warnings.
## Open Questions
- Should `show` also accept `--changes`/`--specs` for discovery without an id? (Out of scope here; current auto-detect and `--type` remain.)
@@ -0,0 +1,57 @@
# Delta: CLI List Command
## MODIFIED Requirements
### Requirement: Command Execution
The command SHALL scan and analyze either active changes or specs based on the selected mode.
#### Scenario: Scanning for changes (default)
- **WHEN** `openspec list` is executed without flags
- **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
#### Scenario: Scanning for specs
- **WHEN** `openspec list --specs` is executed
- **THEN** scan the `openspec/specs/` directory for capabilities
- **AND** read each capability's `spec.md`
- **AND** parse requirements to compute requirement counts
### Requirement: Output Format
The command SHALL display items in a clear, readable table format with mode-appropriate progress or counts.
#### Scenario: Displaying change list (default)
- **WHEN** displaying the list of changes
- **THEN** show a table with columns:
- Change name (directory name)
- Task progress (e.g., "3/5 tasks" or "✓ Complete")
#### Scenario: Displaying spec list
- **WHEN** displaying the list of specs
- **THEN** show a table with columns:
- Spec id (directory name)
- Requirement count (e.g., "requirements 12")
### Requirement: Empty State
The command SHALL provide clear feedback when no items are present for the selected mode.
#### Scenario: Handling empty state (changes)
- **WHEN** no active changes exist (only archive/ or empty changes/)
- **THEN** display: "No active changes found."
#### Scenario: Handling empty state (specs)
- **WHEN** no specs directory exists or contains no capabilities
- **THEN** display: "No specs found."
### Requirement: Flags
The command SHALL accept flags to select the noun being listed.
#### Scenario: Selecting specs
- **WHEN** `--specs` is provided
- **THEN** list specs instead of changes
#### Scenario: Selecting changes
- **WHEN** `--changes` is provided
- **THEN** list changes explicitly (same as default behavior)
@@ -0,0 +1,23 @@
# Delta: OpenSpec Conventions — Verb–Noun CLI Design
## ADDED Requirements
### Requirement: Verb–Noun CLI Command Structure
OpenSpec CLI design SHALL use verbs as top-level commands with nouns provided as arguments or flags for scoping.
#### Scenario: Verb-first command discovery
- **WHEN** a user runs a command like `openspec list`
- **THEN** the verb communicates the action clearly
- **AND** nouns refine scope via flags or arguments (e.g., `--changes`, `--specs`)
#### Scenario: Backward compatibility for noun commands
- **WHEN** users run noun-prefixed commands such as `openspec spec ...` or `openspec change ...`
- **THEN** the CLI SHALL continue to support them for at least one release
- **AND** display a deprecation warning that points to verb-first alternatives
#### Scenario: Disambiguation guidance
- **WHEN** item names are ambiguous between changes and specs
- **THEN** `openspec show` and `openspec validate` SHALL accept `--type spec|change`
- **AND** the help text SHALL document this clearly
@@ -0,0 +1,27 @@
# Implementation Tasks
## 1. CLI Behavior and Help
- [x] 1.1 Un-deprecate top-level `openspec list`; mark `change list` as deprecated with warning that points to `openspec list`
- [x] 1.2 Add support to list specs via `openspec list --specs` and keep `--changes` as default
- [x] 1.3 Update command descriptions and `--help` output to emphasize verb–noun pattern
- [x] 1.4 Keep `openspec spec ...` and `openspec change ...` commands working but print deprecation notices
## 2. Core List Logic
- [x] 2.1 Extend `src/core/list.ts` to accept a mode: `changes` (default) or `specs`
- [x] 2.2 Implement `specs` listing: scan `openspec/specs/*/spec.md`, compute requirement count via parser, format output consistently
- [x] 2.3 Share output structure for both modes; preserve current text table; ensure JSON parity in future change
## 3. Specs and Conventions
- [x] 3.1 Update `openspec/specs/cli-list/spec.md` to document `--specs` (and default to changes)
- [x] 3.2 Update `openspec/specs/openspec-conventions/spec.md` with a requirement for verb–noun CLI design and deprecation guidance
## 4. Tests and Docs
- [x] 4.1 Update tests: ensure `openspec list` works for changes and specs; keep `change list` tests but assert warning
- [ ] 4.2 Update README and any usage docs to show new primary commands
- [ ] 4.3 Add migration notes in repo CHANGELOG or README
## 5. Follow-ups (Optional, not in this change)
- [ ] 5.1 Consider `openspec show --specs/--changes` for discovery without ids
- [ ] 5.2 Consider JSON output for `openspec list` with `--json` for both modes
@@ -0,0 +1,20 @@
## Why
Currently, users must validate changes and specs individually by specifying each ID. This creates friction when:
- Teams want to validate all changes/specs before a release
- Developers need to ensure consistency across multiple related changes
- Users run validation commands without arguments and receive errors instead of helpful guidance
- The subcommand structure requires users to know in advance whether they're validating a change or spec
## What Changes
- Add new top-level `validate` command with intuitive flags (--all, --changes, --specs)
- Enhance existing `change validate` and `spec validate` to support interactive selection (backwards compatibility)
- Interactive selection by default when no arguments provided
- Support direct item validation: `openspec validate <item>` with automatic type detection
## Impact
- New specs to create: cli-validate
- Specs to enhance: cli-change, cli-spec (for backwards compatibility)
- Affected code: src/cli/index.ts, src/commands/validate.ts (new), src/commands/spec.ts, src/commands/change.ts
@@ -0,0 +1,22 @@
# CLI Change Command Spec
## ADDED Requirements
### Requirement: Interactive validation selection
The change validate command SHALL support interactive selection when no change name is provided.
#### Scenario: Interactive change selection for validation
- **WHEN** executing `openspec change validate` without arguments
- **THEN** display an interactive list of available changes
- **AND** allow the user to select a change to validate
- **AND** validate the selected change
#### Scenario: Non-interactive fallback keeps current behavior
- **GIVEN** stdin is not a TTY or `--no-interactive` is provided or environment variable `OPEN_SPEC_INTERACTIVE=0`
- **WHEN** executing `openspec change validate` without a change name
- **THEN** do not prompt interactively
- **AND** print the existing hint including available change IDs
- **AND** set `process.exitCode = 1`
@@ -0,0 +1,23 @@
# CLI Spec Command Spec
## ADDED Requirements
### Requirement: Interactive spec validation
The spec validate command SHALL support interactive selection when no spec-id is provided.
#### Scenario: Interactive spec selection for validation
- **WHEN** executing `openspec spec validate` without arguments
- **THEN** display an interactive list of available specs
- **AND** allow the user to select a spec to validate
- **AND** validate the selected spec
- **AND** maintain all existing validation options (--strict, --json)
#### Scenario: Non-interactive fallback keeps current behavior
- **GIVEN** stdin is not a TTY or `--no-interactive` is provided or environment variable `OPEN_SPEC_INTERACTIVE=0`
- **WHEN** executing `openspec spec validate` without a spec-id
- **THEN** do not prompt interactively
- **AND** print the existing error message for missing spec-id
- **AND** set non-zero exit code
@@ -0,0 +1,149 @@
# CLI Validate Command Spec
## ADDED Requirements
### Requirement: Top-level validate command
The CLI SHALL provide a top-level `validate` command for validating changes and specs with flexible selection options.
#### Scenario: Interactive validation selection
- **WHEN** executing `openspec validate` without arguments
- **THEN** prompt user to select what to validate (all, changes, specs, or specific item)
- **AND** perform validation based on selection
- **AND** display results with appropriate formatting
#### Scenario: Non-interactive environments do not prompt
- **GIVEN** stdin is not a TTY or `--no-interactive` is provided or environment variable `OPEN_SPEC_INTERACTIVE=0`
- **WHEN** executing `openspec validate` without arguments
- **THEN** do not prompt interactively
- **AND** print a helpful hint listing available commands/flags and exit with code 1
#### Scenario: Direct item validation
- **WHEN** executing `openspec validate <item-name>`
- **THEN** automatically detect if item is a change or spec
- **AND** validate the specified item
- **AND** display validation results
### Requirement: Bulk and filtered validation
The validate command SHALL support flags for bulk validation (--all) and filtered validation by type (--changes, --specs).
#### Scenario: Validate everything
- **WHEN** executing `openspec validate --all`
- **THEN** validate all changes in openspec/changes/ (excluding archive)
- **AND** validate all specs in openspec/specs/
- **AND** display a summary showing passed/failed items
- **AND** exit with code 1 if any validation fails
#### Scenario: Scope of bulk validation
- **WHEN** validating with `--all` or `--changes`
- **THEN** include all change proposals under `openspec/changes/`
- **AND** exclude the `openspec/changes/archive/` directory
- **WHEN** validating with `--specs`
- **THEN** include all specs that have a `spec.md` under `openspec/specs/<id>/spec.md`
#### Scenario: Validate all changes
- **WHEN** executing `openspec validate --changes`
- **THEN** validate all changes in openspec/changes/ (excluding archive)
- **AND** display results for each change
- **AND** show summary statistics
#### Scenario: Validate all specs
- **WHEN** executing `openspec validate --specs`
- **THEN** validate all specs in openspec/specs/
- **AND** display results for each spec
- **AND** show summary statistics
### Requirement: Validation options and progress indication
The validate command SHALL support standard validation options (--strict, --json) and display progress during bulk operations.
#### Scenario: Strict validation
- **WHEN** executing `openspec validate --all --strict`
- **THEN** apply strict validation to all items
- **AND** treat warnings as errors
- **AND** fail if any item has warnings or errors
#### Scenario: JSON output
- **WHEN** executing `openspec validate --all --json`
- **THEN** output validation results as JSON
- **AND** include detailed issues for each item
- **AND** include summary statistics
#### Scenario: JSON output schema for bulk validation
- **WHEN** executing `openspec validate --all --json` (or `--changes` / `--specs`)
- **THEN** output a JSON object with the following shape:
- `items`: Array of objects with fields `{ id: string, type: "change"|"spec", valid: boolean, issues: Issue[], durationMs: number }`
- `summary`: Object `{ totals: { items: number, passed: number, failed: number }, byType: { change?: { items: number, passed: number, failed: number }, spec?: { items: number, passed: number, failed: number } } }`
- `version`: String identifier for the schema (e.g., `"1.0"`)
- **AND** exit with code 1 if any `items[].valid === false`
Where `Issue` follows the existing per-item validation report shape `{ level: "ERROR"|"WARNING"|"INFO", path: string, message: string }`.
#### Scenario: Show validation progress
- **WHEN** validating multiple items (--all, --changes, or --specs)
- **THEN** show progress indicator or status updates
- **AND** indicate which item is currently being validated
- **AND** display running count of passed/failed items
#### Scenario: Concurrency limits for performance
- **WHEN** validating multiple items
- **THEN** run validations with a bounded concurrency (e.g., 4–8 in parallel)
- **AND** ensure progress indicators remain responsive
### Requirement: Item type detection and ambiguity handling
The validate command SHALL handle ambiguous names and explicit type overrides to ensure clear, deterministic behavior.
#### Scenario: Direct item validation with automatic type detection
- **WHEN** executing `openspec validate <item-name>`
- **THEN** if `<item-name>` uniquely matches a change or a spec, validate that item
#### Scenario: Ambiguity between change and spec names
- **GIVEN** `<item-name>` exists both as a change and as a spec
- **WHEN** executing `openspec validate <item-name>`
- **THEN** print an ambiguity error explaining both matches
- **AND** suggest passing `--type change` or `--type spec`, or using `openspec change validate` / `openspec spec validate`
- **AND** exit with code 1 without performing validation
#### Scenario: Unknown item name
- **WHEN** the `<item-name>` matches neither a change nor a spec
- **THEN** print a not-found error
- **AND** show nearest-match suggestions when available
- **AND** exit with code 1
#### Scenario: Explicit type override
- **WHEN** executing `openspec validate --type change <item>`
- **THEN** treat `<item>` as a change ID and validate it (skipping auto-detection)
- **WHEN** executing `openspec validate --type spec <item>`
- **THEN** treat `<item>` as a spec ID and validate it (skipping auto-detection)
### Requirement: Interactivity controls
- The CLI SHALL respect `--no-interactive` to disable prompts.
- The CLI SHALL respect `OPEN_SPEC_INTERACTIVE=0` to disable prompts globally.
- Interactive prompts SHALL only be shown when stdin is a TTY and interactivity is not disabled.
#### Scenario: Disabling prompts via flags or environment
- **WHEN** `openspec validate` is executed with `--no-interactive` or with environment `OPEN_SPEC_INTERACTIVE=0`
- **THEN** the CLI SHALL not display interactive prompts
- **AND** SHALL print non-interactive hints or chosen outputs as appropriate
@@ -0,0 +1,81 @@
# Implementation Tasks
## 1. Change Command: Interactive Validation Selection
- [x] 1.1 Add `--no-interactive` flag to `change validate` in `src/cli/index.ts`
- [x] 1.2 Implement interactivity gate respecting TTY and `OPEN_SPEC_INTERACTIVE=0` in `src/commands/change.ts`
- [x] 1.3 When no `[change-name]` is provided and interactivity is allowed, prompt with a list of active changes (exclude `archive/`) and validate the selected one
- [x] 1.4 Preserve current non-interactive fallback: print available change IDs and hint, set `process.exitCode = 1`
- [x] 1.5 Tests: add coverage for interactive and non-interactive flows
- Added `test/commands/change.interactive-validate.test.ts`
## 2. Spec Command: Interactive Validation Selection
- [x] 2.1 Make `spec validate` accept optional `[spec-id]` in `src/commands/spec.ts` registration
- [x] 2.2 Add `--no-interactive` flag to `spec validate`
- [x] 2.3 Implement interactivity gate respecting TTY and `OPEN_SPEC_INTERACTIVE=0`
- [x] 2.4 When no `[spec-id]` provided and interactivity allowed, prompt to select from `openspec/specs/*/spec.md` and validate the selected spec
- [x] 2.5 Preserve current non-interactive fallback when no spec-id and no interactivity: print existing error and exit code non-zero
- [x] 2.6 Tests: add coverage for interactive and non-interactive flows
- Added `test/commands/spec.interactive-validate.test.ts`
## 3. New Top-level `validate` Command
- [x] 3.1 Add `validate` command in `src/cli/index.ts`
- Options: `--all`, `--changes`, `--specs`, `--type <change|spec>`, `--strict`, `--json`, `--no-interactive`
- Usage: `openspec validate [item-name]`
- [x] 3.2 Create `src/commands/validate.ts` implementing:
- [x] 3.2.1 Interactive selector when no args (choices: All, Changes, Specs, Specific item)
- [x] 3.2.2 Non-interactive fallback with helpful hint and exit code 1
- [x] 3.2.3 Direct item validation with automatic type detection
- [x] 3.2.4 Ambiguity error when name exists as both change and spec; suggest `--type` or subcommands
- [x] 3.2.5 Unknown item handling with nearest-match suggestions
- [x] 3.2.6 Bulk validation for `--all`, `--changes`, `--specs` (exclude `openspec/changes/archive/`)
- [x] 3.2.7 Respect `--strict` and `--json` options; JSON shape per spec
- [x] 3.2.8 Exit with code 1 if any validation fails
- [x] 3.2.9 Bounded concurrency (default 4–8) for bulk validation
- [x] 3.2.10 Progress indication during bulk runs (current item, running counts)
## 4. Utilities and Shared Helpers
- [x] 4.1 Add `src/utils/interactive.ts` with `isInteractive(stdin: NodeJS.ReadStream, noInteractiveFlag?: boolean): boolean`
- Considers: `process.stdin.isTTY`, `--no-interactive`, `OPEN_SPEC_INTERACTIVE=0`
- [x] 4.2 Add `src/utils/item-discovery.ts` with:
- `getActiveChangeIds(root = process.cwd()): Promise<string[]>` (exclude `archive/`)
- `getSpecIds(root = process.cwd()): Promise<string[]>` (folders with `spec.md`)
- [ ] 4.3 Optional: `src/utils/concurrency.ts` helper for bounded parallelism
- [x] 4.4 Reuse `src/core/validation/validator.ts` for item validation
## 5. JSON Output (Bulk Validation)
- [x] 5.1 Implement JSON schema:
- `items: Array<{ id: string, type: "change"|"spec", valid: boolean, issues: Issue[], durationMs: number }>`
- `summary: { totals: { items: number, passed: number, failed: number }, byType: { change?: { items: number, passed: number, failed: number }, spec?: { items: number, passed: number, failed: number } } }`
- `version: "1.0"`
- [x] 5.2 Ensure process exit code is 1 if any `items[].valid === false`
- [x] 5.3 Tests for JSON shape (keys, types, counts) and exit code behavior
- Added `test/commands/validate.test.ts`
## 6. Progress and UX
- [x] 6.1 Use `ora` or minimal console progress to show current item and running counts
- [x] 6.2 Keep output stable in `--json` mode (no extra logs to stdout; use stderr for progress if needed)
- [x] 6.3 Ensure responsiveness with concurrency limits
## 7. Tests
- [x] 7.1 Add top-level validate tests: `test/commands/validate.test.ts`
- Includes non-interactive hint, --all JSON, --specs with concurrency, ambiguity error
- [ ] 7.2 Add unit tests for `isInteractive` and item discovery helpers
- [x] 7.3 Extend existing change/spec command tests to cover interactive `validate`
- Added `test/commands/change.interactive-validate.test.ts`, `test/commands/spec.interactive-validate.test.ts`
## 8. CLI Help and Docs
- [x] 8.1 Update command descriptions/options in `src/cli/index.ts`
- [x] 8.2 Verify help output includes `validate` command and flags
- [x] 8.3 Ensure existing specs under `openspec/changes/bulk-validation-interactive-selection/specs/*` remain satisfied
## 9. Non-functional
- [x] 9.1 Code style and types: explicit types for exported APIs; avoid `any`
- [x] 9.2 No linter errors; stable formatting; avoid unrelated refactors
- [x] 9.3 Maintain existing behavior for unaffected commands
## 10. Acceptance Criteria Mapping
- [x] AC-1: `openspec change validate` interactive selection when no arg (TTY only; respects `--no-interactive`/env) — matches cli-change spec
- [x] AC-2: `openspec spec validate` interactive selection when no arg (TTY only; respects `--no-interactive`/env) — matches cli-spec spec
- [x] AC-3: New `openspec validate` supports interactive selection, bulk/filtered validation, JSON schema, progress, concurrency, exit codes — matches cli-validate spec
@@ -0,0 +1,25 @@
# improve-validate-error-messages
## Why
Developers struggle to resolve validation failures because current errors lack actionable guidance. Common issues include: missing deltas, missing required sections, and misformatted scenarios that are silently ignored. Without clear remediation steps, users cannot quickly correct structure or formatting, leading to frustration and rework. Improving error messages with concrete fixes, file/section hints, and suggested commands will significantly reduce time-to-green and make OpenSpec more approachable.
## What Changes
- Validation errors SHALL include specific remediation steps (what to change and where).
- "No deltas found" error SHALL guide users to create `specs/` with proper delta headers and suggest debug commands.
- Missing required sections (Spec: Purpose/Requirements; Change: Why/What Changes) SHALL include expected header names and a minimal skeleton example.
- Likely misformatted scenarios (bulleted WHEN/THEN/AND) SHALL emit a targeted warning explaining the `#### Scenario:` format and show a conversion template.
- All reported issues SHALL include the source file path and structured location (e.g., `deltas[0].requirements[0]`).
- Non-JSON output SHOULD end with a short "Next steps" footer when invalid.
## Impact
- Affected CLI: validate
- Affected code:
- `src/commands/validate.ts`
- `src/core/validation/validator.ts`
- `src/core/validation/constants.ts`
- `src/core/parsers/*` (wrapping thrown errors with richer context)
@@ -0,0 +1,55 @@
# Validate Command
## ADDED Requirements
### Requirement: Validation SHALL provide actionable remediation steps
Validation output SHALL include specific guidance to fix each error, including expected structure, example headers, and suggested commands to verify fixes.
#### Scenario: No deltas found in change
- **WHEN** validating a change with zero parsed deltas
- **THEN** show error "No deltas found" with guidance:
- Ensure `openspec/changes/{id}/specs/` exists with `.md` files
- Use delta headers: `## ADDED Requirements`, `## MODIFIED Requirements`, `## REMOVED Requirements`, `## RENAMED Requirements`
- Each requirement must include at least one `#### Scenario:` block
- Try: `openspec change show {id} --json --deltas-only` to inspect what was parsed
#### Scenario: Missing required sections
- **WHEN** a required section is missing
- **THEN** the validator SHALL include expected header names and a minimal skeleton:
- For Spec: `## Purpose`, `## Requirements`
- For Change: `## Why`, `## What Changes`
- Show an example snippet of the missing section
### Requirement: Validator SHALL detect likely misformatted scenarios and warn with a fix
The validator SHALL recognize bulleted lines that look like scenarios (e.g., lines beginning with WHEN/THEN/AND) and emit a targeted warning with a conversion example to `#### Scenario:`.
#### Scenario: Bulleted WHEN/THEN under a Requirement
- **WHEN** bullets that start with WHEN/THEN/AND are found under a requirement without any `#### Scenario:` headers
- **THEN** emit warning: "Scenarios must use '#### Scenario:' headers", and show a conversion template:
```
#### Scenario: Short name
- **WHEN** ...
- **THEN** ...
- **AND** ...
```
### Requirement: All issues SHALL include file paths and structured locations
Error, warning, and info messages SHALL include:
- Source file path (`openspec/changes/{id}/proposal.md`, `.../specs/{cap}/spec.md`)
- Structured path (e.g., `deltas[0].requirements[0].scenarios`)
#### Scenario: Zod validation error
- **WHEN** a schema validation fails
- **THEN** the message SHALL include `file`, `path`, and a remediation hint if applicable
### Requirement: Invalid results SHALL include a Next steps footer in human-readable output
The CLI SHALL append a Next steps footer when the item is invalid and not using `--json`, including:
- Summary line with counts
- Top-3 guidance bullets (contextual to the most frequent or blocking errors)
- A suggestion to re-run with `--json` and/or the debug command
#### Scenario: Change invalid summary
- **WHEN** a change validation fails
- **THEN** print "Next steps" with 2-3 targeted bullets and suggest `openspec change show <id> --json --deltas-only`
@@ -0,0 +1,21 @@
## 1. Enhance validation messages
- [x] 1.1 Add remediation guidance for "No deltas found"
- [x] 1.2 Include file path and structured path in all issues
- [x] 1.3 Improve messages for missing required sections (Spec, Change)
- [x] 1.4 Detect likely misformatted scenarios and warn with conversion example
- [x] 1.5 Add "Next steps" footer for non-JSON invalid output
## 2. Update constants and helpers
- [x] 2.1 Centralize guidance snippets in `VALIDATION_MESSAGES`
- [x] 2.2 Provide minimal skeleton examples for missing sections
## 3. Parser integration
- [x] 3.1 Capture parser-thrown errors and wrap with richer context
- [x] 3.2 Add file/section references to surfaced parser errors
## 4. Tests
- [x] 4.1 Unit tests for validator message composition
- [x] 4.2 CLI integration tests for human-readable output (with footer)
- [x] 4.3 JSON mode tests (structure unchanged, content enriched)
@@ -0,0 +1,78 @@
# Change: Improve Deterministic Tests (Isolate From Repo State)
## Problem
Some unit tests (e.g., ChangeCommand.show/validate) read the live repository
state via `process.cwd()` and `openspec/changes`. This makes outcomes depend on
whatever directories happen to exist and the order returned by `fs.readdir`,
causing flaky success/failure across environments.
Symptoms observed:
- Tests sometimes select a partial or unrelated change folder.
- Failures like missing `proposal.md` when a stray change directory is picked.
- Environment/sandbox differences alter `readdir` ordering and worker behavior.
## Goals
- Make tests deterministic and hermetic.
- Remove dependence on real repo contents and directory ordering.
- Keep runtime behavior unchanged for end users.
## Non‑Goals
- Introduce heavy frameworks or test harness complexity.
- Redesign CLI behavior or change default paths for users.
## Approach
1) Test-local fixture root
- Each suite that touches filesystem discovery creates a temporary directory:
- `openspec/changes/sample-change/proposal.md`
- `openspec/changes/sample-change/specs/sample/spec.md`
- `beforeAll`: `process.chdir(tmpRoot)`; `afterAll`: restore original cwd.
- Use a constant `changeName = 'sample-change'`; remove reliance on
`readdir` order.
2) Optional thin DI for commands (minimal, if needed)
- Allow `ChangeCommand` (and similar) to accept an optional `root` path
(default `process.cwd()`), used for path resolution.
- Tests pass the temp root explicitly; production code remains unchanged.
3) Harden discovery helpers (safe enhancement)
- Update `getActiveChangeIds()`/`getActiveChanges()` to include only
directories containing `proposal.md` (and optionally at least one
`specs/*/spec.md`).
- Prevents incomplete/stray change folders from being treated as active.
## Rationale
- Small, focused changes eliminate flakiness without altering user workflows.
- Temporary fixtures are a well-understood testing pattern and keep tests fast.
- Optional constructor root param is a minimal DI surface that avoids global
stubbing and keeps code simple.
## Risks & Mitigations
- Risk: Tests forget to restore `process.cwd()`.
- Mitigation: Add `afterAll` guard restoring cwd; reset `process.exitCode` in
`afterEach` where modified.
- Risk: Behavior divergence if DI root is misused.
- Mitigation: Default to `process.cwd()`; only tests pass custom roots.
## Acceptance Criteria
- Tests that previously depended on repo state now:
- Create and use a temp fixture root.
- Do not read real `openspec/changes` during execution.
- Pass consistently regardless of directory order or stray folders.
- No change to CLI behavior for end users (paths still default to cwd).
## Rollout
- Phase 1: Convert the suites that hit `ChangeCommand.show/validate` to
isolated fixtures; verify stability locally and in CI.
- Phase 2: Apply the same pattern to any remaining suites that touch file
discovery (`list`, `show`, `validate`, `diff`).
- Phase 3 (optional): Introduce the constructor `root` param and discovery
hardening, if Phase 1 alone isn’t sufficient.
@@ -0,0 +1,25 @@
# Implementation Tasks
## 1. Test Isolation
- [x] 1.1 Create temp fixture roots per suite (openspec/changes, openspec/specs)
- [x] 1.2 Use process.chdir to temp root within tests
- [x] 1.3 Restore original cwd and clean temp dirs after each
## 2. Deterministic Discovery
- [x] 2.1 Implement getActiveChangeIds(root?) to only include dirs with proposal.md
- [x] 2.2 Implement getSpecIds(root?) to only include dirs with spec.md
- [x] 2.3 Return sorted results to avoid fs.readdir ordering variance
## 3. Command Integration
- [x] 3.1 Ensure change/show/validate rely on cwd and discovery helpers
- [x] 3.2 Keep runtime behavior unchanged for end users
## 4. Validation
- [x] 4.1 Convert affected command tests (show, spec, validate, change) to isolated fixtures
- [x] 4.2 Verify tests pass consistently across environments
- [x] 4.3 Confirm no reads from real repo state during tests
## 5. Optional (Not Needed Now)
- [x] 5.1 Add optional root param to discovery helpers (default process.cwd())
- [ ] 5.2 Consider threading root through command constructors if ever required
@@ -0,0 +1,81 @@
# Remove Diff Command
## Problem
The `openspec diff` command adds unnecessary complexity to the OpenSpec CLI for several reasons:
1. **Redundant functionality**: The `openspec show` command already provides comprehensive visualization of changes through structured JSON output and markdown rendering
2. **Maintenance burden**: The diff command requires a separate dependency (jest-diff) and additional code complexity (~227 lines)
3. **Limited value**: Developers can achieve better diff visualization using existing tools:
- Git diff for actual file changes
- The `show` command for structured change viewing
- Standard diff utilities for comparing spec files directly
4. **Inconsistent with verb-noun pattern**: The command doesn't follow the preferred verb-first command structure that other commands are migrating to
## Solution
Remove the `openspec diff` command entirely and guide users to more appropriate alternatives:
1. **For viewing change content**: Use `openspec show <change-name>` which provides:
- Structured JSON output with `--json` flag
- Markdown rendering for human-readable format
- Delta-only views with `--deltas-only` flag
- Full spec content visualization
2. **For comparing files**: Use standard tools:
- `git diff` for version control comparisons
- System diff utilities for file-by-file comparisons
- IDE diff viewers for visual comparisons
## Benefits
- **Reduced complexity**: Removes ~227 lines of code and the jest-diff dependency
- **Clearer user journey**: Directs users to the canonical `show` command for viewing changes
- **Lower maintenance**: Fewer commands to maintain and test
- **Better alignment**: Focuses on the core OpenSpec workflow without redundant features
## Implementation
### Files to Remove
- `/src/core/diff.ts` - The entire diff command implementation
- `/openspec/specs/cli-diff/spec.md` - The diff command specification
### Files to Update
- `/src/cli/index.ts` - Remove diff command registration (lines 8, 84-96)
- `/package.json` - Remove jest-diff dependency
- `/README.md` - Remove diff command documentation
- `/openspec/README.md` - Remove diff command references
- Various documentation files mentioning `openspec diff`
### Migration Guide for Users
Users currently using `openspec diff` should transition to:
```bash
# Before
openspec diff add-feature
# After - view the change proposal
openspec show add-feature
# After - view only the deltas
openspec show add-feature --json --deltas-only
# After - use git for file comparisons
git diff openspec/specs openspec/changes/add-feature/specs
```
## Risks
- **User disruption**: Existing users may have workflows depending on the diff command
- Mitigation: Provide clear migration guide and deprecation period
- **Loss of visual diff**: The colored, unified diff format will no longer be available
- Mitigation: Users can use git diff or other tools for visual comparisons
## Success Metrics
- Successful removal with no broken dependencies
- Documentation updated to reflect the change
- Tests passing without the diff command
- Reduced package size from removing jest-diff dependency
@@ -0,0 +1,41 @@
# Remove Diff Command - Tasks
## 1. Remove Core Implementation
- [x] Delete `/src/core/diff.ts`
- [x] Remove DiffCommand import from `/src/cli/index.ts`
- [x] Remove diff command registration from CLI
## 2. Remove Specifications
- [x] Delete `/openspec/specs/cli-diff/spec.md`
- [x] Archive the spec for historical reference if needed
## 3. Update Dependencies
- [x] Remove jest-diff from package.json dependencies
- [x] Run pnpm install to update lock file
## 4. Update Documentation
- [x] Update main README.md to remove diff command references
- [x] Update openspec/README.md to remove diff command from command list
- [x] Update CLAUDE.md template if it mentions diff command
- [x] Update any example workflows that use diff command
## 5. Update Related Files
- [x] Search and update any remaining references to "openspec diff" in:
- Template files
- Test files (if any exist for diff command)
- Archive documentation
- Change proposals
## 6. Add Deprecation Notice (Optional Phase)
- [ ] Consider adding a deprecation warning before full removal
- [ ] Provide helpful message directing users to `openspec show` command
## 7. Testing
- [x] Ensure all tests pass after removal
- [x] Verify CLI help text no longer shows diff command
- [x] Test that show command provides adequate replacement functionality
## 8. Documentation of Alternative Workflows
- [x] Document how to use `openspec show` for viewing changes
- [x] Document how to use git diff for file comparisons
- [x] Add migration guide to help text or documentation
@@ -0,0 +1,130 @@
# Design: Agent Instructions Update
## Approach
### Information Architecture
- **Front-load critical information** - Three-stage workflow comes first
- **Clear hierarchy** - Core Workflow → Quick Start → Commands → Details → Edge Cases
- **50% length reduction** - Target ~285 lines from current ~575 lines
- **Imperative mood** - "Create proposal" vs "You should create a proposal"
- **Bullet points over paragraphs** - Scannable, concise information
### Three-Stage Workflow Documentation
The workflow is now prominently featured as a core concept:
1. **Creating** - Proposal generation phase
2. **Implementing** - Code development phase with explicit steps:
- Read proposal.md for understanding
- Read design.md for technical context
- Read tasks.md for checklist
- Implement tasks sequentially
- Mark complete immediately after each task
3. **Archiving** - Post-deployment finalization phase
This structure helps agents understand the lifecycle and their role at each stage. The implementation phase is particularly detailed to prevent common mistakes like skipping documentation or batching task completion.
### CLI Documentation Updates
- **Comprehensive command coverage** - All 9 primary commands documented
- **`openspec list` prominence** - Essential for discovering changes and specs
- **Interactive mode documentation** - How agents can use prompts effectively
- **Complete flag documentation** - All options like --json, --type, --skip-specs
- **Deprecation cleanup** - Remove noun-first patterns (openspec change show)
### Agent-Specific Enhancements
Based on industry best practices for coding agents (Claude Code, Cursor, etc.):
**Implementation Workflow**
- Explicit steps prevent skipping critical context
- Reading proposal/design first ensures understanding before coding
- Sequential task completion maintains focus
- Immediate marking prevents losing track of progress
- Addresses common failure mode: jumping straight to code
**Spec Discovery Workflow**
- Always check existing specs before creating new ones
- Use `openspec list --specs` to discover current capabilities
- Prefer modifying existing specs over creating duplicates
- Prevents fragmentation and maintains coherent architecture
**Decision Clarity**
- Clear decision trees eliminating ambiguous conditions
- Concrete examples for each decision branch
- Simplified bug vs feature determination
**Tool Usage Guidance**
- Tool selection matrix (when to use Grep vs Glob vs Read)
- Error recovery patterns for common failures
- Verification workflows to confirm correctness
**Context Management**
- "Before Any Task" checklist for gathering context
- What to read before starting any work
- How to maintain state across interactions
**Spec File Structure Documentation**
- Complete examples with ADDED/MODIFIED/REMOVED sections
- Critical scenario formatting (#### Scenario: headers)
- Delta file location clarity (changes/{name}/specs/)
- Addresses most common creation errors from retrospective
**Troubleshooting and Debugging**
- Common error messages with solutions
- Delta detection debugging steps
- Validation best practices
- JSON output for inspection
- Prevents hours of frustration from silent failures
**Best Practices**
- Be concise (one-line answers when appropriate)
- Be specific (file.ts:42 line references)
- Start simple (<100 lines, single-file defaults)
- Justify complexity (require metrics/data)
## Design Rationale
### Why These Changes Matter
**Cognitive Load Reduction**
- Agents process instructions better with clear structure
- Front-loading critical info reduces scanning time
- Decision trees eliminate analysis paralysis
**Industry Alignment**
- Follows patterns proven effective in Claude Code, Cursor, GitHub Copilot
- Addresses common failure modes (ambiguous decisions, missing context)
- Optimizes for LLM strengths (pattern matching) vs weaknesses (calculations)
**Addressing Critical Pain Points (from Retrospective)**
- **Scenario formatting** - Biggest struggle, now explicitly documented with examples
- **Complete spec structure** - Full examples prevent structural errors
- **Delta detection issues** - Debugging commands help diagnose problems
- **Silent parsing failures** - Troubleshooting section explains common issues
**Practical Impact**
- Faster agent comprehension of tasks
- Fewer misinterpretations of requirements
- More consistent implementation quality
- Better error recovery when things go wrong
- Prevents the most common errors identified in user experience
## Trade-offs
### What We're Removing
- Lengthy explanations of concepts that can be inferred
- Redundant examples that don't add clarity
- Verbose edge case documentation (moved to reference section)
- Deprecated command documentation
### What We're Keeping
- All critical workflow steps
- Complete CLI command reference
- Complexity management principles
- Directory structure visualization
- Quick reference summary
## Implementation Notes
The CLAUDE.md template is intentionally more concise than README.md since:
- It appears in every project root
- Agents can reference the full README.md for details
- It needs to load quickly in AI context windows
- Focus is on immediate actionable guidance
@@ -0,0 +1,117 @@
# Update OpenSpec Agent Instructions
## Why
The current OpenSpec agent instructions need updates to follow best practices for AI assistant instructions (brevity, clarity, removing ambiguity), ensure CLI commands are current with the actual implementation, and properly document the three-stage workflow pattern that agents should follow.
## What Changes
### Core Structure Improvements
- **Front-load the 3-stage workflow** as the primary mental model:
1. Creating a change proposal (proposal.md, spec deltas, design.md, tasks.md)
2. Implementing a change proposal:
- First read proposal.md to understand the change
- Read design.md if it exists for technical context
- Read tasks.md for the implementation checklist
- Complete tasks one by one
- Mark each task complete immediately after finishing
3. Archiving the change proposal (using archive command after deployment)
- **Reduce instruction length by 50%** while maintaining all critical information
- **Restructure with clear hierarchy**: Core Workflow → Quick Start → Commands → Details → Edge Cases
### Decision Clarity Enhancements
- **Add clear decision trees** for common scenarios (bug vs feature, proposal needed vs not)
- **Remove ambiguous conditions** that confuse agent decision-making
- **Add "Before Any Task" checklist** for context gathering
- **Add "Before Creating Specs" rule** - Always check existing specs first to avoid duplicates
### CLI Documentation Updates
- **Complete command documentation** with all current functionality:
- `openspec init [path]` - Initialize OpenSpec in a project
- `openspec list` - List all active changes (default)
- `openspec list --specs` - List all specifications
- `openspec show [item]` - Display change or spec with auto-detection
- `openspec show` - Interactive mode for selection
- `openspec diff [change]` - Show spec differences for a change
- `openspec validate [item]` - Validate changes or specs
- `openspec archive [change]` - Archive completed change after deployment
- `openspec update [path]` - Update OpenSpec instruction files
- **Document all flags and options**:
- `--json` output format for programmatic use
- `--type change|spec` for disambiguation
- `--skip-specs` for tooling-only archives
- `--strict` for strict validation mode
- `--no-interactive` to disable prompts
- **Remove deprecated command references** (noun-first patterns like `openspec change show`)
- **Add concrete examples** for each command variation
- **Document debugging commands**:
- `openspec show [change] --json --deltas-only` for inspecting deltas
- `openspec validate [change] --strict` for comprehensive validation
### Spec File Structure Documentation
- **Complete spec file examples** showing proper structure:
```markdown
## ADDED Requirements
### Requirement: Clear requirement statement
The system SHALL provide the functionality...
#### Scenario: Descriptive scenario name
- **WHEN** condition occurs
- **THEN** expected outcome
- **AND** additional outcomes
```
- **Scenario formatting requirements** (critical - most common error):
- MUST use `#### Scenario:` headers (4 hashtags)
- NOT bullet lists or bold text
- Each requirement MUST have at least one scenario
- **Delta file location** - Clear explanation:
- Spec files go in `changes/{name}/specs/` directory
- Deltas are automatically extracted from these files
- Use operation prefixes: ADDED, MODIFIED, REMOVED, RENAMED
### Troubleshooting Section
- **Common errors and solutions**:
- "Change must have at least one delta" → Check specs/ directory exists with .md files
- "Requirement must have at least one scenario" → Check scenario uses `#### Scenario:` format
- Silent scenario parsing failures → Verify exact header format
- **Delta detection debugging**:
- Use `openspec show [change] --json --deltas-only` to inspect parsed deltas
- Check that spec files have operation prefixes (## ADDED Requirements)
- Verify specs/ subdirectory structure
- **Validation best practices**:
- Always use `--strict` flag for comprehensive checks
- Use JSON output for debugging: `--json | jq '.deltas'`
### Agent-Specific Improvements
- **Implementation workflow** - Clear step-by-step process:
1. Read proposal.md to understand what's being built
2. Read design.md (if exists) for technical decisions
3. Read tasks.md for the implementation checklist
4. Implement tasks one by one in order
5. Mark each task complete immediately: `- [x] Task completed`
6. Never skip ahead or batch task completion
- **Spec discovery workflow** - Always check existing specs before creating new ones:
- Use `openspec list --specs` to see all current specs
- Check if capability already exists before creating
- Prefer modifying existing specs over creating duplicates
- **Tool selection matrix** - When to use Grep vs Glob vs Read
- **Error recovery patterns** - How to handle common failures
- **Context management guide** - What to read before starting tasks
- **Verification workflows** - How to confirm changes are correct
### Best Practices Section
- **Be concise** - One-line answers when appropriate
- **Be specific** - Use exact file paths and line numbers (file.ts:42)
- **Start simple** - Default to <100 lines, single-file implementations
- **Justify complexity** - Require data/metrics for any optimization
## Impact
- Affected specs: None (this is a tooling/documentation change)
- Affected code:
- `src/core/templates/claude-template.ts` - Update CLAUDE.md template
- Affected documentation:
- `openspec/README.md` - Main OpenSpec instructions
- CLAUDE.md files generated by `openspec init` command
Note: This is a tooling/infrastructure change that doesn't require spec updates. When archiving, use `openspec archive update-agent-instructions --skip-specs`.
@@ -0,0 +1,69 @@
# Implementation Tasks
## 1. Restructure OpenSpec README.md
- [x] 1.1 Front-load the three-stage workflow as primary content
- [x] 1.2 Restructure with hierarchy: Core Workflow → Quick Start → Commands → Details → Edge Cases
- [x] 1.3 Reduce total length by 50% (target: ~285 lines from current ~575)
- [x] 1.4 Add "Before Any Task" context-gathering checklist
- [x] 1.5 Add "Before Creating Specs" rule to check existing specs first
## 2. Add Decision Clarity
- [x] 2.1 Create clear decision trees for "Create Proposal?" scenarios
- [x] 2.2 Remove ambiguous conditions that confuse agents
- [x] 2.3 Add concrete examples for each decision branch
- [x] 2.4 Simplify bug vs feature determination logic
- [x] 2.5 Add explicit Stage 2 implementation steps (read → implement → mark complete)
## 3. Update CLI Documentation
- [x] 3.1 Document `openspec list` and `openspec list --specs` commands
- [x] 3.2 Document `openspec show` with all flags and interactive mode
- [x] 3.3 Document `openspec diff [change]` for viewing spec differences
- [x] 3.4 Document `openspec archive` with --skip-specs option
- [x] 3.5 Document `openspec validate` with --strict and batch modes
- [x] 3.6 Document `openspec init` and `openspec update` commands
- [x] 3.7 Remove all deprecated noun-first command references
- [x] 3.8 Add concrete usage examples for each command variation
- [x] 3.9 Document all flags: --json, --type, --no-interactive, etc.
- [x] 3.10 Document debugging commands: `show --json --deltas-only`
## 4. Add Spec File Documentation
- [x] 4.1 Add complete spec file structure example with ADDED/MODIFIED sections
- [x] 4.2 Document scenario formatting requirements (#### Scenario: headers)
- [x] 4.3 Explain delta file location (changes/{name}/specs/ directory)
- [x] 4.4 Show how deltas are automatically extracted
- [x] 4.5 Include warning about most common error (scenario formatting)
## 5. Add Troubleshooting Section
- [x] 5.1 Document common errors and their solutions
- [x] 5.2 Add delta detection debugging steps
- [x] 5.3 Include validation best practices (--strict flag)
- [x] 5.4 Show how to use JSON output for debugging
- [x] 5.5 Add examples of silent parsing failures
## 6. Add Agent-Specific Sections
- [x] 6.1 Add implementation workflow (read docs → implement tasks → mark complete)
- [x] 6.2 Add spec discovery workflow (check existing before creating)
- [x] 6.3 Create tool selection matrix (Grep vs Glob vs Read)
- [x] 6.4 Add error recovery patterns section
- [x] 6.5 Add context management guide
- [x] 6.6 Add verification workflows section
- [x] 6.7 Add best practices section (concise, specific, simple)
## 7. Update CLAUDE.md Template
- [x] 7.1 Update `src/core/templates/claude-template.ts` with streamlined content
- [x] 7.2 Include three-stage workflow prominently
- [x] 7.3 Add comprehensive CLI quick reference (list, show, diff, archive, etc.)
- [x] 7.4 Add "Before Any Task" checklist
- [x] 7.5 Add "Before Creating Specs" rule
- [x] 7.6 Keep complexity management principles
- [x] 7.7 Add critical scenario formatting note (#### Scenario: headers)
- [x] 7.8 Include debugging command reference
## 8. Testing and Validation
- [x] 8.1 Test all documented CLI commands for accuracy
- [x] 8.2 Run `openspec init` to verify CLAUDE.md generation
- [x] 8.3 Validate instruction clarity with example scenarios
- [x] 8.4 Ensure no critical information was lost in streamlining
- [x] 8.5 Verify decision trees eliminate ambiguity
- [x] 8.6 Test scenario formatting examples work correctly
- [x] 8.7 Verify troubleshooting steps resolve common errors
+75 -20
View File
@@ -10,9 +10,7 @@ openspec archive [change-name] [--yes|-y]
Options:
- `--yes`, `-y`: Skip confirmation prompts (for automation)
## Requirements
### Requirement: Change Selection
The command SHALL support both interactive and direct change selection methods.
@@ -72,26 +70,25 @@ The archive operation SHALL follow a structured process to safely move changes t
### Requirement: Spec Update Process
Before moving the change to archive, the command SHALL update main specs to reflect the deployed reality.
Before moving the change to archive, the command SHALL apply delta changes to main specs to reflect the deployed reality.
#### Scenario: Updating specs from change
#### Scenario: Applying delta changes
- **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
- **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: No specs in change
#### Scenario: Validating delta changes
- **WHEN** no specs exist in the change
- **THEN** skip the spec update step
- **AND** proceed with archiving
- **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: Confirmation Behavior
@@ -129,8 +126,6 @@ The spec update confirmation SHALL provide clear visibility into changes before
- **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.
@@ -144,6 +139,66 @@ The command SHALL handle various error conditions gracefully.
- Archive target already exists
- File system permissions issues
### Requirement: Skip Specs Option
The archive command SHALL support a `--skip-specs` flag that skips all spec update operations and proceeds directly to archiving.
#### Scenario: Skipping spec updates with flag
- **WHEN** executing `openspec archive <change> --skip-specs`
- **THEN** skip spec discovery and update confirmation
- **AND** proceed directly to moving the change to archive
- **AND** display a message indicating specs were skipped
### Requirement: Non-blocking confirmation
The archive operation SHALL proceed when the user declines spec updates instead of cancelling the entire operation.
#### Scenario: User declines spec update confirmation
- **WHEN** the user declines spec update confirmation
- **THEN** skip spec updates
- **AND** continue with the archive operation
- **AND** display a success message indicating specs were not updated
### 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
```
### Requirement: Archive Validation
The archive command SHALL validate changes before applying them to ensure data integrity.
#### Scenario: Pre-archive validation
- **WHEN** executing `openspec archive change-name`
- **THEN** validate the change structure first
- **AND** only proceed if validation passes
- **AND** show validation errors if it fails
#### Scenario: Force archive without validation
- **WHEN** executing `openspec archive change-name --no-validate`
- **THEN** skip validation (unsafe mode)
- **AND** show warning about skipping validation
## Why These Decisions
**Interactive selection**: Reduces typing and helps users see available changes
+91
View File
@@ -0,0 +1,91 @@
# cli-change Specification
## Purpose
TBD - created by archiving change add-change-commands. Update Purpose after archive.
## Requirements
### Requirement: Change Command
The system SHALL provide a `change` command with subcommands for displaying, listing, and validating change proposals.
#### Scenario: Show change as JSON
- **WHEN** executing `openspec change show update-error --json`
- **THEN** parse the markdown change file
- **AND** extract change structure and deltas
- **AND** output valid JSON to stdout
#### Scenario: List all changes
- **WHEN** executing `openspec change list`
- **THEN** scan the openspec/changes directory
- **AND** return list of all pending changes
- **AND** support JSON output with `--json` flag
#### Scenario: Show only requirement changes
- **WHEN** executing `openspec change show update-error --requirements-only`
- **THEN** display only the requirement changes (ADDED/MODIFIED/REMOVED/RENAMED)
- **AND** exclude why and what changes sections
#### Scenario: Validate change structure
- **WHEN** executing `openspec change validate update-error`
- **THEN** parse the change file
- **AND** validate against Zod schema
- **AND** ensure deltas are well-formed
### Requirement: Legacy Compatibility
The system SHALL maintain backward compatibility with the existing `list` command while showing deprecation notices.
#### Scenario: Legacy list command
- **WHEN** executing `openspec list`
- **THEN** display current list of changes (existing behavior)
- **AND** show deprecation notice: "Note: 'openspec list' is deprecated. Use 'openspec change list' instead."
#### Scenario: Legacy list with --all flag
- **WHEN** executing `openspec list --all`
- **THEN** display all changes (existing behavior)
- **AND** show same deprecation notice
### Requirement: Interactive show selection
The change show command SHALL support interactive selection when no change name is provided.
#### Scenario: Interactive change selection for show
- **WHEN** executing `openspec change show` without arguments
- **THEN** display an interactive list of available changes
- **AND** allow the user to select a change to show
- **AND** display the selected change content
- **AND** maintain all existing show options (--json, --deltas-only)
#### Scenario: Non-interactive fallback keeps current behavior
- **GIVEN** stdin is not a TTY or `--no-interactive` is provided or environment variable `OPEN_SPEC_INTERACTIVE=0`
- **WHEN** executing `openspec change show` without a change name
- **THEN** do not prompt interactively
- **AND** print the existing hint including available change IDs
- **AND** set `process.exitCode = 1`
### Requirement: Interactive validation selection
The change validate command SHALL support interactive selection when no change name is provided.
#### Scenario: Interactive change selection for validation
- **WHEN** executing `openspec change validate` without arguments
- **THEN** display an interactive list of available changes
- **AND** allow the user to select a change to validate
- **AND** validate the selected change
#### Scenario: Non-interactive fallback keeps current behavior
- **GIVEN** stdin is not a TTY or `--no-interactive` is provided or environment variable `OPEN_SPEC_INTERACTIVE=0`
- **WHEN** executing `openspec change validate` without a change name
- **THEN** do not prompt interactively
- **AND** print the existing hint including available change IDs
- **AND** set `process.exitCode = 1`
-120
View File
@@ -1,120 +0,0 @@
# CLI Diff Command Specification
## Purpose
The `openspec diff` command provides developers with a visual comparison between proposed spec changes and the current deployed specs.
## Command Syntax
```bash
openspec diff [change-name]
```
## Requirements
### Requirement: Without Arguments
The command SHALL provide an interactive selection when no change is specified.
#### Scenario: Running without arguments
- **WHEN** running `openspec diff` without arguments
- **THEN** list all available changes in the `changes/` directory (excluding archive)
- **AND** prompt user to select a change
### Requirement: With Change Name
The command SHALL compare specs when a specific change is provided.
#### Scenario: Running with change name
- **WHEN** running `openspec diff <change-name>`
- **THEN** compare all spec files in `changes/<change-name>/specs/` with corresponding files in `specs/`
### Requirement: Diff Output
The command SHALL generate appropriate diff output for all spec changes.
#### Scenario: Comparing existing files
- **WHEN** file exists in both locations
- **THEN** show unified diff
#### Scenario: New files
- **WHEN** file only exists in change
- **THEN** show as new file (all lines with +)
#### Scenario: Deleted files
- **WHEN** file only exists in current specs
- **THEN** show as deleted (all lines with -)
### Requirement: Display Format
The command SHALL use standard unified diff format for consistency with existing tools.
#### Scenario: Formatting diff output
- **WHEN** displaying diff output
- **THEN** use standard unified diff format:
- Lines prefixed with `-` for removed content
- Lines prefixed with `+` for added content
- Lines without prefix for unchanged context
- File headers showing the paths being compared
### Requirement: Color Support
The command SHALL enhance readability with colors when supported.
#### Scenario: Terminal with color support
- **WHEN** terminal supports colors
- **THEN** display:
- Removed lines in red
- Added lines in green
- File headers in bold
- Context lines in default color
### Requirement: Error Handling
The command SHALL provide clear error messages for various failure conditions.
#### Scenario: Change not found
- **WHEN** specified change doesn't exist
- **THEN** display error "Change '<name>' not found"
#### Scenario: No specs in change
- **WHEN** no specs directory in change
- **THEN** display "No spec changes found for '<name>'"
#### Scenario: Missing changes directory
- **WHEN** changes directory doesn't exist
- **THEN** display "No OpenSpec changes directory found"
## Examples
```bash
# View diff for specific change
$ openspec diff add-auth-feature
--- specs/user-auth/spec.md
+++ changes/add-auth-feature/specs/user-auth/spec.md
@@ -10,6 +10,8 @@
Users SHALL authenticate with email and password.
+Users MAY authenticate with OAuth providers.
+
WHEN credentials are valid THEN issue JWT token.
# List all changes and select
$ openspec diff
Available changes:
1. add-auth-feature
2. update-payment-flow
3. add-status-command
Select a change (1-3):
```
+34 -27
View File
@@ -3,20 +3,22 @@
## Purpose
The `openspec list` command SHALL provide developers with a quick overview of all active changes in the project, showing their names and task completion status.
## Requirements
### Requirement: Command Execution
The command SHALL scan and analyze either active changes or specs based on the selected mode.
The command SHALL scan and analyze all active changes to provide a comprehensive overview.
#### Scenario: Scanning for changes
- **WHEN** `openspec list` is executed
#### Scenario: Scanning for changes (default)
- **WHEN** `openspec list` is executed without flags
- **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
#### Scenario: Scanning for specs
- **WHEN** `openspec list --specs` is executed
- **THEN** scan the `openspec/specs/` directory for capabilities
- **AND** read each capability's `spec.md`
- **AND** parse requirements to compute requirement counts
### Requirement: Task Counting
The command SHALL accurately count task completion status using standard markdown checkbox patterns.
@@ -30,37 +32,42 @@ The command SHALL accurately count task completion status using standard markdow
- **AND** calculate total tasks as the sum of completed and incomplete
### Requirement: Output Format
The command SHALL display items in a clear, readable table format with mode-appropriate progress or counts.
The command SHALL display changes in a clear, readable table format with progress indicators.
#### Scenario: Displaying change list
- **WHEN** displaying the list
#### Scenario: Displaying change list (default)
- **WHEN** displaying the list of changes
- **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
```
#### Scenario: Displaying spec list
- **WHEN** displaying the list of specs
- **THEN** show a table with columns:
- Spec id (directory name)
- Requirement count (e.g., "requirements 12")
### Requirement: Flags
The command SHALL accept flags to select the noun being listed.
#### Scenario: Selecting specs
- **WHEN** `--specs` is provided
- **THEN** list specs instead of changes
#### Scenario: Selecting changes
- **WHEN** `--changes` is provided
- **THEN** list changes explicitly (same as default behavior)
### Requirement: Empty State
The command SHALL provide clear feedback when no items are present for the selected mode.
The command SHALL provide clear feedback when no active changes are present.
#### Scenario: Handling empty state
#### Scenario: Handling empty state (changes)
- **WHEN** no active changes exist (only archive/ or empty changes/)
- **THEN** display: "No active changes found."
#### Scenario: Handling empty state (specs)
- **WHEN** no specs directory exists or contains no capabilities
- **THEN** display: "No specs found."
### Requirement: Error Handling
The command SHALL gracefully handle missing files and directories with appropriate messages.
+85
View File
@@ -0,0 +1,85 @@
# cli-show Specification
## Purpose
TBD - created by archiving change add-interactive-show-command. Update Purpose after archive.
## Requirements
### Requirement: Top-level show command
The CLI SHALL provide a top-level `show` command for displaying changes and specs with intelligent selection.
#### Scenario: Interactive show selection
- **WHEN** executing `openspec show` without arguments
- **THEN** prompt user to select type (change or spec)
- **AND** display list of available items for selected type
- **AND** show the selected item's content
#### Scenario: Non-interactive environments do not prompt
- **GIVEN** stdin is not a TTY or `--no-interactive` is provided or environment variable `OPEN_SPEC_INTERACTIVE=0`
- **WHEN** executing `openspec show` without arguments
- **THEN** do not prompt
- **AND** print a helpful hint with examples for `openspec show <item>` or `openspec change/spec show`
- **AND** exit with code 1
#### Scenario: Direct item display
- **WHEN** executing `openspec show <item-name>`
- **THEN** automatically detect if item is a change or spec
- **AND** display the item's content
- **AND** use appropriate formatting based on item type
#### Scenario: Type detection and ambiguity handling
- **WHEN** executing `openspec show <item-name>`
- **THEN** if `<item-name>` uniquely matches a change or a spec, show that item
- **AND** if it matches both, print an ambiguity error and suggest `--type change|spec` or using `openspec change show`/`openspec spec show`
- **AND** if it matches neither, print not-found with nearest-match suggestions
#### Scenario: Explicit type override
- **WHEN** executing `openspec show --type change <item>`
- **THEN** treat `<item>` as a change ID and show it (skipping auto-detection)
- **WHEN** executing `openspec show --type spec <item>`
- **THEN** treat `<item>` as a spec ID and show it (skipping auto-detection)
### Requirement: Output format options
The show command SHALL support various output formats consistent with existing commands.
#### Scenario: JSON output
- **WHEN** executing `openspec show <item> --json`
- **THEN** output the item in JSON format
- **AND** include parsed metadata and structure
- **AND** maintain format consistency with existing change/spec show commands
#### Scenario: Flag scoping and delegation
- **WHEN** showing a change or a spec via the top-level command
- **THEN** accept common flags such as `--json`
- **AND** pass through type-specific flags to the corresponding implementation
- Change-only flags: `--deltas-only` (alias `--requirements-only` deprecated)
- Spec-only flags: `--requirements`, `--no-scenarios`, `-r/--requirement`
- **AND** ignore irrelevant flags for the detected type with a warning
### Requirement: Interactivity controls
- The CLI SHALL respect `--no-interactive` to disable prompts.
- The CLI SHALL respect `OPEN_SPEC_INTERACTIVE=0` to disable prompts globally.
- Interactive prompts SHALL only be shown when stdin is a TTY and interactivity is not disabled.
#### Scenario: Change-specific options
- **WHEN** showing a change with `openspec show <change-name> --deltas-only`
- **THEN** display only the deltas in JSON format
- **AND** maintain compatibility with existing change show options
#### Scenario: Spec-specific options
- **WHEN** showing a spec with `openspec show <spec-id> --requirements`
- **THEN** display only requirements in JSON format
- **AND** support other spec options (--no-scenarios, -r)
- **AND** maintain compatibility with existing spec show options
+87
View File
@@ -0,0 +1,87 @@
# cli-spec Specification
## Purpose
TBD - created by archiving change add-interactive-show-command. Update Purpose after archive.
## Requirements
### Requirement: Interactive spec show
The spec show command SHALL support interactive selection when no spec-id is provided.
#### Scenario: Interactive spec selection for show
- **WHEN** executing `openspec spec show` without arguments
- **THEN** display an interactive list of available specs
- **AND** allow the user to select a spec to show
- **AND** display the selected spec content
- **AND** maintain all existing show options (--json, --requirements, --no-scenarios, -r)
#### Scenario: Non-interactive fallback keeps current behavior
- **GIVEN** stdin is not a TTY or `--no-interactive` is provided or environment variable `OPEN_SPEC_INTERACTIVE=0`
- **WHEN** executing `openspec spec show` without a spec-id
- **THEN** do not prompt interactively
- **AND** print the existing error message for missing spec-id
- **AND** set non-zero exit code
### Requirement: Spec Command
The system SHALL provide a `spec` command with subcommands for displaying, listing, and validating specifications.
#### Scenario: Show spec as JSON
- **WHEN** executing `openspec spec show init --json`
- **THEN** parse the markdown spec file
- **AND** extract headings and content hierarchically
- **AND** output valid JSON to stdout
#### Scenario: List all specs
- **WHEN** executing `openspec spec list`
- **THEN** scan the openspec/specs directory
- **AND** return list of all available capabilities
- **AND** support JSON output with `--json` flag
#### Scenario: Filter spec content
- **WHEN** executing `openspec spec show init --requirements`
- **THEN** display only requirement names and SHALL statements
- **AND** exclude scenario content
#### Scenario: Validate spec structure
- **WHEN** executing `openspec spec validate init`
- **THEN** parse the spec file
- **AND** validate against Zod schema
- **AND** report any structural issues
### Requirement: JSON Schema Definition
The system SHALL define Zod schemas that accurately represent the spec structure for runtime validation.
#### Scenario: Schema validation
- **WHEN** parsing a spec into JSON
- **THEN** validate the structure using Zod schemas
- **AND** ensure all required fields are present
- **AND** provide clear error messages for validation failures
### Requirement: Interactive spec validation
The spec validate command SHALL support interactive selection when no spec-id is provided.
#### Scenario: Interactive spec selection for validation
- **WHEN** executing `openspec spec validate` without arguments
- **THEN** display an interactive list of available specs
- **AND** allow the user to select a spec to validate
- **AND** validate the selected spec
- **AND** maintain all existing validation options (--strict, --json)
#### Scenario: Non-interactive fallback keeps current behavior
- **GIVEN** stdin is not a TTY or `--no-interactive` is provided or environment variable `OPEN_SPEC_INTERACTIVE=0`
- **WHEN** executing `openspec spec validate` without a spec-id
- **THEN** do not prompt interactively
- **AND** print the existing error message for missing spec-id
- **AND** set non-zero exit code
+23 -3
View File
@@ -3,9 +3,7 @@
## 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
## Requirements
### Requirement: Update Behavior
The update command SHALL update OpenSpec instruction files to the latest templates in a team-friendly manner.
@@ -48,6 +46,28 @@ The update command SHALL handle file updates in a predictable and safe manner.
- **AND** be idempotent (repeated runs have no additional effect)
- **AND** respect team members' AI tool choices by not creating unwanted files
### Requirement: Tool-Agnostic Updates
The update command SHALL update only existing AI tool configuration files and SHALL NOT create new ones.
#### Scenario: Updating existing tool files
- **WHEN** a user runs `openspec update`
- **THEN** update each AI tool configuration file that exists (e.g., CLAUDE.md, COPILOT.md)
- **AND** do not create missing tool configuration files
- **AND** preserve user content outside OpenSpec markers
### Requirement: Core Files Always Updated
The update command SHALL always update the core OpenSpec files and display an ASCII-safe success message.
#### Scenario: Successful update
- **WHEN** the update completes successfully
- **THEN** replace `openspec/README.md` with the latest template
- **AND** update existing AI tool configuration files within markers
- **AND** display the message: "Updated OpenSpec instructions"
## Edge Cases
### Requirement: Error Handling
+201
View File
@@ -0,0 +1,201 @@
# cli-validate Specification
## Purpose
TBD - created by archiving change improve-validate-error-messages. Update Purpose after archive.
## Requirements
### Requirement: Validation SHALL provide actionable remediation steps
Validation output SHALL include specific guidance to fix each error, including expected structure, example headers, and suggested commands to verify fixes.
#### Scenario: No deltas found in change
- **WHEN** validating a change with zero parsed deltas
- **THEN** show error "No deltas found" with guidance:
- Ensure `openspec/changes/{id}/specs/` exists with `.md` files
- Use delta headers: `## ADDED Requirements`, `## MODIFIED Requirements`, `## REMOVED Requirements`, `## RENAMED Requirements`
- Each requirement must include at least one `#### Scenario:` block
- Try: `openspec change show {id} --json --deltas-only` to inspect what was parsed
#### Scenario: Missing required sections
- **WHEN** a required section is missing
- **THEN** the validator SHALL include expected header names and a minimal skeleton:
- For Spec: `## Purpose`, `## Requirements`
- For Change: `## Why`, `## What Changes`
- Show an example snippet of the missing section
### Requirement: Validator SHALL detect likely misformatted scenarios and warn with a fix
The validator SHALL recognize bulleted lines that look like scenarios (e.g., lines beginning with WHEN/THEN/AND) and emit a targeted warning with a conversion example to `#### Scenario:`.
#### Scenario: Bulleted WHEN/THEN under a Requirement
- **WHEN** bullets that start with WHEN/THEN/AND are found under a requirement without any `#### Scenario:` headers
- **THEN** emit warning: "Scenarios must use '#### Scenario:' headers", and show a conversion template:
```
#### Scenario: Short name
- **WHEN** ...
- **THEN** ...
- **AND** ...
```
### Requirement: All issues SHALL include file paths and structured locations
Error, warning, and info messages SHALL include:
- Source file path (`openspec/changes/{id}/proposal.md`, `.../specs/{cap}/spec.md`)
- Structured path (e.g., `deltas[0].requirements[0].scenarios`)
#### Scenario: Zod validation error
- **WHEN** a schema validation fails
- **THEN** the message SHALL include `file`, `path`, and a remediation hint if applicable
### Requirement: Invalid results SHALL include a Next steps footer in human-readable output
The CLI SHALL append a Next steps footer when the item is invalid and not using `--json`, including:
- Summary line with counts
- Top-3 guidance bullets (contextual to the most frequent or blocking errors)
- A suggestion to re-run with `--json` and/or the debug command
#### Scenario: Change invalid summary
- **WHEN** a change validation fails
- **THEN** print "Next steps" with 2-3 targeted bullets and suggest `openspec change show <id> --json --deltas-only`
### Requirement: Top-level validate command
The CLI SHALL provide a top-level `validate` command for validating changes and specs with flexible selection options.
#### Scenario: Interactive validation selection
- **WHEN** executing `openspec validate` without arguments
- **THEN** prompt user to select what to validate (all, changes, specs, or specific item)
- **AND** perform validation based on selection
- **AND** display results with appropriate formatting
#### Scenario: Non-interactive environments do not prompt
- **GIVEN** stdin is not a TTY or `--no-interactive` is provided or environment variable `OPEN_SPEC_INTERACTIVE=0`
- **WHEN** executing `openspec validate` without arguments
- **THEN** do not prompt interactively
- **AND** print a helpful hint listing available commands/flags and exit with code 1
#### Scenario: Direct item validation
- **WHEN** executing `openspec validate <item-name>`
- **THEN** automatically detect if item is a change or spec
- **AND** validate the specified item
- **AND** display validation results
### Requirement: Bulk and filtered validation
The validate command SHALL support flags for bulk validation (--all) and filtered validation by type (--changes, --specs).
#### Scenario: Validate everything
- **WHEN** executing `openspec validate --all`
- **THEN** validate all changes in openspec/changes/ (excluding archive)
- **AND** validate all specs in openspec/specs/
- **AND** display a summary showing passed/failed items
- **AND** exit with code 1 if any validation fails
#### Scenario: Scope of bulk validation
- **WHEN** validating with `--all` or `--changes`
- **THEN** include all change proposals under `openspec/changes/`
- **AND** exclude the `openspec/changes/archive/` directory
- **WHEN** validating with `--specs`
- **THEN** include all specs that have a `spec.md` under `openspec/specs/<id>/spec.md`
#### Scenario: Validate all changes
- **WHEN** executing `openspec validate --changes`
- **THEN** validate all changes in openspec/changes/ (excluding archive)
- **AND** display results for each change
- **AND** show summary statistics
#### Scenario: Validate all specs
- **WHEN** executing `openspec validate --specs`
- **THEN** validate all specs in openspec/specs/
- **AND** display results for each spec
- **AND** show summary statistics
### Requirement: Validation options and progress indication
The validate command SHALL support standard validation options (--strict, --json) and display progress during bulk operations.
#### Scenario: Strict validation
- **WHEN** executing `openspec validate --all --strict`
- **THEN** apply strict validation to all items
- **AND** treat warnings as errors
- **AND** fail if any item has warnings or errors
#### Scenario: JSON output
- **WHEN** executing `openspec validate --all --json`
- **THEN** output validation results as JSON
- **AND** include detailed issues for each item
- **AND** include summary statistics
#### Scenario: JSON output schema for bulk validation
- **WHEN** executing `openspec validate --all --json` (or `--changes` / `--specs`)
- **THEN** output a JSON object with the following shape:
- `items`: Array of objects with fields `{ id: string, type: "change"|"spec", valid: boolean, issues: Issue[], durationMs: number }`
- `summary`: Object `{ totals: { items: number, passed: number, failed: number }, byType: { change?: { items: number, passed: number, failed: number }, spec?: { items: number, passed: number, failed: number } } }`
- `version`: String identifier for the schema (e.g., `"1.0"`)
- **AND** exit with code 1 if any `items[].valid === false`
Where `Issue` follows the existing per-item validation report shape `{ level: "ERROR"|"WARNING"|"INFO", path: string, message: string }`.
#### Scenario: Show validation progress
- **WHEN** validating multiple items (--all, --changes, or --specs)
- **THEN** show progress indicator or status updates
- **AND** indicate which item is currently being validated
- **AND** display running count of passed/failed items
#### Scenario: Concurrency limits for performance
- **WHEN** validating multiple items
- **THEN** run validations with a bounded concurrency (e.g., 4–8 in parallel)
- **AND** ensure progress indicators remain responsive
### Requirement: Item type detection and ambiguity handling
The validate command SHALL handle ambiguous names and explicit type overrides to ensure clear, deterministic behavior.
#### Scenario: Direct item validation with automatic type detection
- **WHEN** executing `openspec validate <item-name>`
- **THEN** if `<item-name>` uniquely matches a change or a spec, validate that item
#### Scenario: Ambiguity between change and spec names
- **GIVEN** `<item-name>` exists both as a change and as a spec
- **WHEN** executing `openspec validate <item-name>`
- **THEN** print an ambiguity error explaining both matches
- **AND** suggest passing `--type change` or `--type spec`, or using `openspec change validate` / `openspec spec validate`
- **AND** exit with code 1 without performing validation
#### Scenario: Unknown item name
- **WHEN** the `<item-name>` matches neither a change nor a spec
- **THEN** print a not-found error
- **AND** show nearest-match suggestions when available
- **AND** exit with code 1
#### Scenario: Explicit type override
- **WHEN** executing `openspec validate --type change <item>`
- **THEN** treat `<item>` as a change ID and validate it (skipping auto-detection)
- **WHEN** executing `openspec validate --type spec <item>`
- **THEN** treat `<item>` as a spec ID and validate it (skipping auto-detection)
### Requirement: Interactivity controls
- The CLI SHALL respect `--no-interactive` to disable prompts.
- The CLI SHALL respect `OPEN_SPEC_INTERACTIVE=0` to disable prompts globally.
- Interactive prompts SHALL only be shown when stdin is a TTY and interactivity is not disabled.
#### Scenario: Disabling prompts via flags or environment
- **WHEN** `openspec validate` is executed with `--no-interactive` or with environment `OPEN_SPEC_INTERACTIVE=0`
- **THEN** the CLI SHALL not display interactive prompts
- **AND** SHALL print non-interactive hints or chosen outputs as appropriate
+220 -2
View File
@@ -3,6 +3,226 @@
## 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.
## Requirements
### Requirement: Structured conventions for specs and changes
OpenSpec conventions SHALL mandate a structured spec format with clear requirement and scenario sections so tooling can parse consistently.
#### Scenario: Following the structured spec format
- **WHEN** writing or updating OpenSpec specifications
- **THEN** authors SHALL use `### Requirement: ...` followed by at least one `#### Scenario: ...` section
### Requirement: Project Structure
An OpenSpec project SHALL maintain a consistent directory structure for specifications and changes.
#### Scenario: Initializing project structure
- **WHEN** an OpenSpec project is initialized
- **THEN** it SHALL have this structure:
```
openspec/
├── project.md # Project-specific context
├── README.md # AI assistant instructions
├── specs/ # Current deployed capabilities
│ └── [capability]/ # Single, focused capability
│ ├── spec.md # WHAT and WHY
│ └── design.md # HOW (optional, for established patterns)
└── changes/ # Proposed changes
├── [change-name]/ # Descriptive change identifier
│ ├── proposal.md # Why, what, and impact
│ ├── tasks.md # Implementation checklist
│ ├── design.md # Technical decisions (optional)
│ └── specs/ # Complete future state
│ └── [capability]/
│ └── spec.md # Clean markdown (no diff syntax)
└── archive/ # Completed changes
└── YYYY-MM-DD-[name]/
```
### 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: Header-Based Requirement Identification
Requirement headers SHALL serve as unique identifiers for programmatic matching between current specs and proposed changes.
#### Scenario: Matching requirements programmatically
- **WHEN** processing delta changes
- **THEN** use the `### Requirement: [Name]` header as the unique identifier
- **AND** match using normalized headers: `normalize(header) = trim(header)`
- **AND** compare headers with case-sensitive equality after normalization
#### Scenario: Handling requirement renames
- **WHEN** renaming a requirement
- **THEN** use a special `## RENAMED Requirements` section
- **AND** specify both old and new names explicitly:
```markdown
## RENAMED Requirements
- FROM: `### Requirement: Old Name`
- TO: `### Requirement: New Name`
```
- **AND** if content also changes, include under MODIFIED using the NEW header
#### Scenario: Validating header uniqueness
- **WHEN** creating or modifying requirements
- **THEN** ensure no duplicate headers exist within a spec
- **AND** validation tools SHALL flag duplicate headers as errors
### Requirement: Change Storage Convention
Change proposals SHALL store only the additions, modifications, and removals to specifications, not complete future states.
#### Scenario: Creating change proposals with additions
- **WHEN** creating a change proposal that adds new requirements
- **THEN** include only the new requirements under `## ADDED Requirements`
- **AND** each requirement SHALL include its complete content
- **AND** use the standard structured format for requirements and scenarios
#### Scenario: Creating change proposals with modifications
- **WHEN** creating a change proposal that modifies existing requirements
- **THEN** include the modified requirements under `## MODIFIED Requirements`
- **AND** use the same header text as in the current spec (normalized)
- **AND** include the complete modified requirement (not a diff)
- **AND** optionally annotate what changed with inline comments like `← (was X)`
#### Scenario: Creating change proposals with removals
- **WHEN** creating a change proposal that removes requirements
- **THEN** list them under `## REMOVED Requirements`
- **AND** use the normalized header text for identification
- **AND** include reason for removal
- **AND** document any migration path if applicable
The `changes/[name]/specs/` directory SHALL contain:
- Delta files showing only what changes
- Sections for ADDED, MODIFIED, REMOVED, and RENAMED requirements
- Normalized header matching for requirement identification
- Complete requirements using the structured format
- Clear indication of change type for each requirement
#### Scenario: Using standard output symbols
- **WHEN** displaying delta operations in CLI output
- **THEN** use these standard symbols:
- `+` for ADDED (green)
- `~` for MODIFIED (yellow)
- `-` for REMOVED (red)
- `→` for RENAMED (cyan)
### Requirement: Archive Process Enhancement
The archive process SHALL programmatically apply delta changes to current specifications using header-based matching.
#### Scenario: Archiving changes with deltas
- **WHEN** archiving a completed change
- **THEN** the archive command SHALL:
1. Parse RENAMED sections first and apply renames
2. Parse REMOVED sections and remove by normalized header match
3. Parse MODIFIED sections and replace by normalized header match (using new names if renamed)
4. Parse ADDED sections and append new requirements
- **AND** validate that all MODIFIED/REMOVED headers exist in current spec
- **AND** validate that ADDED headers don't already exist
- **AND** generate the updated spec in the main specs/ directory
#### Scenario: Handling conflicts during archive
- **WHEN** delta changes conflict with current spec state
- **THEN** the archive command SHALL report specific conflicts
- **AND** require manual resolution before proceeding
- **AND** provide clear guidance on resolving conflicts
### Requirement: Proposal Format
Proposals SHALL explicitly document all changes with clear from/to comparisons.
#### Scenario: Documenting changes
- **WHEN** documenting what changes
- **THEN** the proposal SHALL explicitly describe each change:
```markdown
**[Section or Behavior Name]**
- From: [current state/requirement]
- To: [future state/requirement]
- Reason: [why this change is needed]
- Impact: [breaking/non-breaking, who's affected]
```
This explicit format compensates for not having inline diffs and ensures reviewers understand exactly what will change.
### 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
### Requirement: Structured Format Adoption
Behavioral specifications SHALL adopt the structured format with `### Requirement:` and `#### Scenario:` headers as the default.
#### Scenario: Use structured headings for behavior
- **WHEN** documenting behavioral requirements
- **THEN** use `### Requirement:` for requirements
- **AND** use `#### Scenario:` for scenarios with bold WHEN/THEN/AND keywords
### Requirement: Verb–Noun CLI Command Structure
OpenSpec CLI design SHALL use verbs as top-level commands with nouns provided as arguments or flags for scoping.
#### Scenario: Verb-first command discovery
- **WHEN** a user runs a command like `openspec list`
- **THEN** the verb communicates the action clearly
- **AND** nouns refine scope via flags or arguments (e.g., `--changes`, `--specs`)
#### Scenario: Backward compatibility for noun commands
- **WHEN** users run noun-prefixed commands such as `openspec spec ...` or `openspec change ...`
- **THEN** the CLI SHALL continue to support them for at least one release
- **AND** display a deprecation warning that points to verb-first alternatives
#### Scenario: Disambiguation guidance
- **WHEN** item names are ambiguous between changes and specs
- **THEN** `openspec show` and `openspec validate` SHALL accept `--type spec|change`
- **AND** the help text SHALL document this clearly
## Core Principles
@@ -73,7 +293,6 @@ Behavioral specifications SHALL use a structured format with consistent section
- Sub-bullets provide examples or specifics
- Keep sub-bullets concise
## Change Storage Convention
### Requirement: Header-Based Requirement Identification
@@ -132,7 +351,6 @@ Change proposals SHALL store only the additions, modifications, and removals to
- **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
+13 -7
View File
@@ -1,6 +1,6 @@
{
"name": "openspec",
"version": "0.0.1",
"name": "@fission-ai/openspec",
"version": "0.1.0",
"description": "AI-native system for spec-driven development",
"keywords": [
"openspec",
@@ -9,14 +9,18 @@
"ai",
"development"
],
"homepage": "https://github.com/tabdilsaidixit/openspec",
"homepage": "https://github.com/Fission-AI/OpenSpec",
"repository": {
"type": "git",
"url": "git+https://github.com/tabdilsaidixit/openspec.git"
"url": "https://github.com/Fission-AI/OpenSpec"
},
"license": "MIT",
"author": "OpenSpec Contributors",
"type": "module",
"publishConfig": {
"access": "public",
"tag": "next"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
@@ -41,12 +45,15 @@
"test:watch": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest --coverage",
"prepare": "npm run build"
"prepare": "pnpm run build",
"prepublishOnly": "pnpm run build",
"changeset": "changeset"
},
"engines": {
"node": ">=20.19.0"
},
"devDependencies": {
"@changesets/cli": "^2.27.7",
"@types/node": "^24.2.0",
"@vitest/ui": "^3.2.4",
"typescript": "^5.9.2",
@@ -56,8 +63,7 @@
"@inquirer/prompts": "^7.8.0",
"chalk": "^5.5.0",
"commander": "^14.0.0",
"jest-diff": "^30.0.5",
"ora": "^8.2.0",
"zod": "^4.0.17"
}
}
}
+744 -70
View File
File diff suppressed because it is too large Load Diff
+77 -24
View File
@@ -1,21 +1,25 @@
import { Command } from 'commander';
import { createRequire } from 'module';
import ora from 'ora';
import path from 'path';
import { promises as fs } from 'fs';
import { InitCommand } from '../core/init.js';
import { UpdateCommand } from '../core/update.js';
import { DiffCommand } from '../core/diff.js';
import { ListCommand } from '../core/list.js';
import { ArchiveCommand } from '../core/archive.js';
import { registerSpecCommand } from '../commands/spec.js';
import { ChangeCommand } from '../commands/change.js';
import { ValidateCommand } from '../commands/validate.js';
import { ShowCommand } from '../commands/show.js';
const program = new Command();
const require = createRequire(import.meta.url);
const { version } = require('../../package.json');
program
.name('openspec')
.description('AI-native system for spec-driven development')
.version('0.0.1');
.version(version);
// Global options
program.option('--no-color', 'Disable color output');
@@ -76,28 +80,16 @@ program
}
});
program
.command('diff [change-name]')
.description('Show differences between proposed spec changes and current specs (includes validation warnings)')
.action(async (changeName?: string) => {
try {
const diffCommand = new DiffCommand();
await diffCommand.execute(changeName);
} catch (error) {
console.log(); // Empty line for spacing
ora().fail(`Error: ${(error as Error).message}`);
process.exit(1);
}
});
program
.command('list')
.description('List all active changes with their task status (DEPRECATED: use "openspec change list" instead)')
.action(async () => {
.description('List items (changes by default). Use --specs to list specs.')
.option('--specs', 'List specs instead of changes')
.option('--changes', 'List changes explicitly (default)')
.action(async (options?: { specs?: boolean; changes?: boolean }) => {
try {
console.log('\x1b[33m%s\x1b[0m', 'Warning: The "openspec list" command is deprecated. Please use "openspec change list" instead.\n');
const listCommand = new ListCommand();
await listCommand.execute();
const mode: 'changes' | 'specs' = options?.specs ? 'specs' : 'changes';
await listCommand.execute('.', mode);
} catch (error) {
console.log(); // Empty line for spacing
ora().fail(`Error: ${(error as Error).message}`);
@@ -110,13 +102,19 @@ const changeCmd = program
.command('change')
.description('Manage OpenSpec change proposals');
// Deprecation notice for noun-based commands
changeCmd.hook('preAction', () => {
console.error('Warning: The "openspec change ..." commands are deprecated. Prefer verb-first commands (e.g., "openspec list", "openspec validate --changes").');
});
changeCmd
.command('show [change-name]')
.description('Show a change proposal in JSON or markdown format')
.option('--json', 'Output as JSON')
.option('--deltas-only', 'Show only deltas (JSON only)')
.option('--requirements-only', 'Alias for --deltas-only (deprecated)')
.action(async (changeName?: string, options?: { json?: boolean; requirementsOnly?: boolean; deltasOnly?: boolean }) => {
.option('--no-interactive', 'Disable interactive prompts')
.action(async (changeName?: string, options?: { json?: boolean; requirementsOnly?: boolean; deltasOnly?: boolean; noInteractive?: boolean }) => {
try {
const changeCommand = new ChangeCommand();
await changeCommand.show(changeName, options);
@@ -128,11 +126,12 @@ changeCmd
changeCmd
.command('list')
.description('List all active changes')
.description('List all active changes (DEPRECATED: use "openspec list" instead)')
.option('--json', 'Output as JSON')
.option('--long', 'Show id and title with counts')
.action(async (options?: { json?: boolean; long?: boolean }) => {
try {
console.error('Warning: "openspec change list" is deprecated. Use "openspec list".');
const changeCommand = new ChangeCommand();
await changeCommand.list(options);
} catch (error) {
@@ -146,10 +145,14 @@ changeCmd
.description('Validate a change proposal')
.option('--strict', 'Enable strict validation mode')
.option('--json', 'Output validation report as JSON')
.action(async (changeName?: string, options?: { strict?: boolean; json?: boolean }) => {
.option('--no-interactive', 'Disable interactive prompts')
.action(async (changeName?: string, options?: { strict?: boolean; json?: boolean; noInteractive?: boolean }) => {
try {
const changeCommand = new ChangeCommand();
await changeCommand.validate(changeName, options);
if (typeof process.exitCode === 'number' && process.exitCode !== 0) {
process.exit(process.exitCode);
}
} catch (error) {
console.error(`Error: ${(error as Error).message}`);
process.exitCode = 1;
@@ -175,4 +178,54 @@ program
registerSpecCommand(program);
program.parse();
// Top-level validate command
program
.command('validate [item-name]')
.description('Validate changes and specs')
.option('--all', 'Validate all changes and specs')
.option('--changes', 'Validate all changes')
.option('--specs', 'Validate all specs')
.option('--type <type>', 'Specify item type when ambiguous: change|spec')
.option('--strict', 'Enable strict validation mode')
.option('--json', 'Output validation results as JSON')
.option('--concurrency <n>', 'Max concurrent validations (defaults to env OPENSPEC_CONCURRENCY or 6)')
.option('--no-interactive', 'Disable interactive prompts')
.action(async (itemName?: string, options?: { all?: boolean; changes?: boolean; specs?: boolean; type?: string; strict?: boolean; json?: boolean; noInteractive?: boolean; concurrency?: string }) => {
try {
const validateCommand = new ValidateCommand();
await validateCommand.execute(itemName, options);
} catch (error) {
console.log();
ora().fail(`Error: ${(error as Error).message}`);
process.exit(1);
}
});
// Top-level show command
program
.command('show [item-name]')
.description('Show a change or spec')
.option('--json', 'Output as JSON')
.option('--type <type>', 'Specify item type when ambiguous: change|spec')
.option('--no-interactive', 'Disable interactive prompts')
// change-only flags
.option('--deltas-only', 'Show only deltas (JSON only, change)')
.option('--requirements-only', 'Alias for --deltas-only (deprecated, change)')
// spec-only flags
.option('--requirements', 'JSON only: Show only requirements (exclude scenarios)')
.option('--no-scenarios', 'JSON only: Exclude scenario content')
.option('-r, --requirement <id>', 'JSON only: Show specific requirement by ID (1-based)')
// allow unknown options to pass-through to underlying command implementation
.allowUnknownOption(true)
.action(async (itemName?: string, options?: { json?: boolean; type?: string; noInteractive?: boolean; [k: string]: any }) => {
try {
const showCommand = new ShowCommand();
await showCommand.execute(itemName, options ?? {});
} catch (error) {
console.log();
ora().fail(`Error: ${(error as Error).message}`);
process.exit(1);
}
});
program.parse();
+68 -25
View File
@@ -1,9 +1,12 @@
import { promises as fs } from 'fs';
import path from 'path';
import { select } from '@inquirer/prompts';
import { JsonConverter } from '../core/converters/json-converter.js';
import { Validator } from '../core/validation/validator.js';
import { ChangeParser } from '../core/parsers/change-parser.js';
import { Change } from '../core/schemas/index.js';
import { isInteractive } from '../utils/interactive.js';
import { getActiveChangeIds } from '../utils/item-discovery.js';
// Constants for better maintainability
const ARCHIVE_DIR = 'archive';
@@ -23,19 +26,28 @@ export class ChangeCommand {
* - JSON mode: minimal object with deltas; --deltas-only returns same object with filtered deltas
* Note: --requirements-only is deprecated alias for --deltas-only
*/
async show(changeName?: string, options?: { json?: boolean; requirementsOnly?: boolean; deltasOnly?: boolean }): Promise<void> {
async show(changeName?: string, options?: { json?: boolean; requirementsOnly?: boolean; deltasOnly?: boolean; noInteractive?: boolean }): Promise<void> {
const changesPath = path.join(process.cwd(), 'openspec', 'changes');
if (!changeName) {
const canPrompt = isInteractive(options?.noInteractive);
const changes = await this.getActiveChanges(changesPath);
if (changes.length === 0) {
console.error('No change specified. No active changes found.');
if (canPrompt && changes.length > 0) {
const selected = await select({
message: 'Select a change to show',
choices: changes.map(id => ({ name: id, value: id })),
});
changeName = selected;
} else {
console.error(`No change specified. Available IDs: ${changes.join(', ')}`);
if (changes.length === 0) {
console.error('No change specified. No active changes found.');
} else {
console.error(`No change specified. Available IDs: ${changes.join(', ')}`);
}
console.error('Hint: use "openspec change list" to view available changes.');
process.exitCode = 1;
return;
}
console.error('Hint: use "openspec change list" to view available changes.');
process.exitCode = 1;
return;
}
const proposalPath = path.join(changesPath, changeName, 'proposal.md');
@@ -170,31 +182,40 @@ export class ChangeCommand {
}
}
async validate(changeName?: string, options?: { strict?: boolean; json?: boolean }): Promise<void> {
async validate(changeName?: string, options?: { strict?: boolean; json?: boolean; noInteractive?: boolean }): Promise<void> {
const changesPath = path.join(process.cwd(), 'openspec', 'changes');
if (!changeName) {
const changes = await this.getActiveChanges(changesPath);
if (changes.length === 0) {
console.error('No change specified. No active changes found.');
const canPrompt = isInteractive(options?.noInteractive);
const changes = await getActiveChangeIds();
if (canPrompt && changes.length > 0) {
const selected = await select({
message: 'Select a change to validate',
choices: changes.map(id => ({ name: id, value: id })),
});
changeName = selected;
} else {
console.error(`No change specified. Available IDs: ${changes.join(', ')}`);
if (changes.length === 0) {
console.error('No change specified. No active changes found.');
} else {
console.error(`No change specified. Available IDs: ${changes.join(', ')}`);
}
console.error('Hint: use "openspec change list" to view available changes.');
process.exitCode = 1;
return;
}
console.error('Hint: use "openspec change list" to view available changes.');
process.exitCode = 1;
return;
}
const proposalPath = path.join(changesPath, changeName, 'proposal.md');
const changeDir = path.join(changesPath, changeName);
try {
await fs.access(proposalPath);
await fs.access(changeDir);
} catch {
throw new Error(`Change "${changeName}" not found at ${proposalPath}`);
throw new Error(`Change "${changeName}" not found at ${changeDir}`);
}
const validator = new Validator(options?.strict || false);
const report = await validator.validateChange(proposalPath);
const report = await validator.validateChangeDeltaSpecs(changeDir);
if (options?.json) {
console.log(JSON.stringify(report, null, 2));
@@ -202,12 +223,17 @@ export class ChangeCommand {
if (report.valid) {
console.log(`Change "${changeName}" is valid`);
} else {
console.error(`Change "${changeName}" has validation issues`);
console.error(`Change "${changeName}" has issues`);
report.issues.forEach(issue => {
const label = issue.level === 'ERROR' ? 'ERROR' : 'WARNING';
const prefix = issue.level === 'ERROR' ? '✗' : '⚠';
console.error(`${prefix} [${label}] ${issue.path}: ${issue.message}`);
});
// Next steps footer to guide fixing issues
this.printNextSteps();
if (!options?.json) {
process.exitCode = 1;
}
}
}
}
@@ -215,10 +241,18 @@ export class ChangeCommand {
private async getActiveChanges(changesPath: string): Promise<string[]> {
try {
const entries = await fs.readdir(changesPath, { withFileTypes: true });
return entries
.filter(entry => entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== ARCHIVE_DIR)
.map(entry => entry.name)
.sort();
const result: string[] = [];
for (const entry of entries) {
if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === ARCHIVE_DIR) continue;
const proposalPath = path.join(changesPath, entry.name, 'proposal.md');
try {
await fs.access(proposalPath);
result.push(entry.name);
} catch {
// skip directories without proposal.md
}
}
return result.sort();
} catch {
return [];
}
@@ -245,4 +279,13 @@ export class ChangeCommand {
return { total, completed };
}
}
private printNextSteps(): void {
const bullets: string[] = [];
bullets.push('- Ensure change has deltas in specs/: use headers ## ADDED/MODIFIED/REMOVED/RENAMED Requirements');
bullets.push('- Each requirement MUST include at least one #### Scenario: block');
bullets.push('- Debug parsed deltas: openspec change show <id> --json --deltas-only');
console.error('Next steps:');
bullets.forEach(b => console.error(` ${b}`));
}
}
+139
View File
@@ -0,0 +1,139 @@
import { select } from '@inquirer/prompts';
import path from 'path';
import { isInteractive } from '../utils/interactive.js';
import { getActiveChangeIds, getSpecIds } from '../utils/item-discovery.js';
import { ChangeCommand } from './change.js';
import { SpecCommand } from './spec.js';
import { nearestMatches } from '../utils/match.js';
type ItemType = 'change' | 'spec';
const CHANGE_FLAG_KEYS = new Set(['deltasOnly', 'requirementsOnly']);
const SPEC_FLAG_KEYS = new Set(['requirements', 'scenarios', 'requirement']);
export class ShowCommand {
async execute(itemName?: string, options: { json?: boolean; type?: string; noInteractive?: boolean; [k: string]: any } = {}): Promise<void> {
const interactive = isInteractive(options.noInteractive);
const typeOverride = this.normalizeType(options.type);
if (!itemName) {
if (interactive) {
const type = await select<ItemType>({
message: 'What would you like to show?',
choices: [
{ name: 'Change', value: 'change' as const },
{ name: 'Spec', value: 'spec' as const },
],
});
await this.runInteractiveByType(type, options);
return;
}
this.printNonInteractiveHint();
process.exitCode = 1;
return;
}
await this.showDirect(itemName, { typeOverride, options });
}
private normalizeType(value?: string): ItemType | undefined {
if (!value) return undefined;
const v = value.toLowerCase();
if (v === 'change' || v === 'spec') return v;
return undefined;
}
private async runInteractiveByType(type: ItemType, options: { json?: boolean; noInteractive?: boolean; [k: string]: any }): Promise<void> {
if (type === 'change') {
const changes = await getActiveChangeIds();
if (changes.length === 0) {
console.error('No changes found.');
process.exitCode = 1;
return;
}
const picked = await select<string>({ message: 'Pick a change', choices: changes.map(id => ({ name: id, value: id })) });
const cmd = new ChangeCommand();
await cmd.show(picked, options as any);
return;
}
const specs = await getSpecIds();
if (specs.length === 0) {
console.error('No specs found.');
process.exitCode = 1;
return;
}
const picked = await select<string>({ message: 'Pick a spec', choices: specs.map(id => ({ name: id, value: id })) });
const cmd = new SpecCommand();
await cmd.show(picked, options as any);
}
private async showDirect(itemName: string, params: { typeOverride?: ItemType; options: { json?: boolean; [k: string]: any } }): Promise<void> {
// Optimize lookups when type is pre-specified
let isChange = false;
let isSpec = false;
let changes: string[] = [];
let specs: string[] = [];
if (params.typeOverride === 'change') {
changes = await getActiveChangeIds();
isChange = changes.includes(itemName);
} else if (params.typeOverride === 'spec') {
specs = await getSpecIds();
isSpec = specs.includes(itemName);
} else {
[changes, specs] = await Promise.all([getActiveChangeIds(), getSpecIds()]);
isChange = changes.includes(itemName);
isSpec = specs.includes(itemName);
}
const resolvedType = params.typeOverride ?? (isChange ? 'change' : isSpec ? 'spec' : undefined);
if (!resolvedType) {
console.error(`Unknown item '${itemName}'`);
const suggestions = nearestMatches(itemName, [...changes, ...specs]);
if (suggestions.length) console.error(`Did you mean: ${suggestions.join(', ')}?`);
process.exitCode = 1;
return;
}
if (!params.typeOverride && isChange && isSpec) {
console.error(`Ambiguous item '${itemName}' matches both a change and a spec.`);
console.error('Pass --type change|spec, or use: openspec change show / openspec spec show');
process.exitCode = 1;
return;
}
this.warnIrrelevantFlags(resolvedType, params.options);
if (resolvedType === 'change') {
const cmd = new ChangeCommand();
await cmd.show(itemName, params.options as any);
return;
}
const cmd = new SpecCommand();
await cmd.show(itemName, params.options as any);
}
private printNonInteractiveHint(): void {
console.error('Nothing to show. Try one of:');
console.error(' openspec show <item>');
console.error(' openspec change show');
console.error(' openspec spec show');
console.error('Or run in an interactive terminal.');
}
private warnIrrelevantFlags(type: ItemType, options: { [k: string]: any }): boolean {
const irrelevant: string[] = [];
if (type === 'change') {
for (const k of SPEC_FLAG_KEYS) if (k in options) irrelevant.push(k);
} else {
for (const k of CHANGE_FLAG_KEYS) if (k in options) irrelevant.push(k);
}
if (irrelevant.length > 0) {
console.error(`Warning: Ignoring flags not applicable to ${type}: ${irrelevant.join(', ')}`);
return true;
}
return false;
}
}
+126 -82
View File
@@ -4,102 +4,132 @@ import { join } from 'path';
import { MarkdownParser } from '../core/parsers/markdown-parser.js';
import { Validator } from '../core/validation/validator.js';
import type { Spec } from '../core/schemas/index.js';
import { select } from '@inquirer/prompts';
import { isInteractive } from '../utils/interactive.js';
import { getSpecIds } from '../utils/item-discovery.js';
const SPECS_DIR = 'openspec/specs';
interface ShowOptions {
json?: boolean;
// JSON-only filters (raw-first text has no filters)
requirements?: boolean;
scenarios?: boolean; // --no-scenarios sets this to false (JSON only)
requirement?: string; // JSON only
noInteractive?: boolean;
}
function parseSpecFromFile(specPath: string, specId: string): Spec {
const content = readFileSync(specPath, 'utf-8');
const parser = new MarkdownParser(content);
return parser.parseSpec(specId);
}
function validateRequirementIndex(spec: Spec, requirementOpt?: string): number | undefined {
if (!requirementOpt) return undefined;
const index = Number.parseInt(requirementOpt, 10);
if (!Number.isInteger(index) || index < 1 || index > spec.requirements.length) {
throw new Error(`Requirement ${requirementOpt} not found`);
}
return index - 1; // convert to 0-based
}
function filterSpec(spec: Spec, options: ShowOptions): Spec {
const requirementIndex = validateRequirementIndex(spec, options.requirement);
const includeScenarios = options.scenarios !== false && !options.requirements;
const filteredRequirements = (requirementIndex !== undefined
? [spec.requirements[requirementIndex]]
: spec.requirements
).map(req => ({
text: req.text,
scenarios: includeScenarios ? req.scenarios : [],
}));
const metadata = spec.metadata ?? { version: '1.0.0', format: 'openspec' as const };
return {
name: spec.name,
overview: spec.overview,
requirements: filteredRequirements,
metadata,
};
}
/**
* Print the raw markdown content for a spec file without any formatting.
* Raw-first behavior ensures text mode is a passthrough for deterministic output.
*/
function printSpecTextRaw(specPath: string): void {
const content = readFileSync(specPath, 'utf-8');
console.log(content);
}
export class SpecCommand {
private SPECS_DIR = 'openspec/specs';
async show(specId?: string, options: ShowOptions = {}): Promise<void> {
if (!specId) {
const canPrompt = isInteractive(options?.noInteractive);
const specIds = await getSpecIds();
if (canPrompt && specIds.length > 0) {
specId = await select({
message: 'Select a spec to show',
choices: specIds.map(id => ({ name: id, value: id })),
});
} else {
throw new Error('Missing required argument <spec-id>');
}
}
const specPath = join(this.SPECS_DIR, specId, 'spec.md');
if (!existsSync(specPath)) {
throw new Error(`Spec '${specId}' not found at openspec/specs/${specId}/spec.md`);
}
if (options.json) {
if (options.requirements && options.requirement) {
throw new Error('Options --requirements and --requirement cannot be used together');
}
const parsed = parseSpecFromFile(specPath, specId);
const filtered = filterSpec(parsed, options);
const output = {
id: specId,
title: parsed.name,
overview: parsed.overview,
requirementCount: filtered.requirements.length,
requirements: filtered.requirements,
metadata: parsed.metadata ?? { version: '1.0.0', format: 'openspec' as const },
};
console.log(JSON.stringify(output, null, 2));
return;
}
printSpecTextRaw(specPath);
}
}
export function registerSpecCommand(rootProgram: typeof program) {
const specCommand = rootProgram
.command('spec')
.description('Manage and view OpenSpec specifications');
interface ShowOptions {
json?: boolean;
// JSON-only filters (raw-first text has no filters)
requirements?: boolean;
scenarios?: boolean; // --no-scenarios sets this to false (JSON only)
requirement?: string; // JSON only
}
function parseSpecFromFile(specPath: string, specId: string): Spec {
const content = readFileSync(specPath, 'utf-8');
const parser = new MarkdownParser(content);
return parser.parseSpec(specId);
}
function validateRequirementIndex(spec: Spec, requirementOpt?: string): number | undefined {
if (!requirementOpt) return undefined;
const index = Number.parseInt(requirementOpt, 10);
if (!Number.isInteger(index) || index < 1 || index > spec.requirements.length) {
throw new Error(`Requirement ${requirementOpt} not found`);
}
return index - 1; // convert to 0-based
}
function filterSpec(spec: Spec, options: ShowOptions): Spec {
const requirementIndex = validateRequirementIndex(spec, options.requirement);
const includeScenarios = options.scenarios !== false && !options.requirements;
const filteredRequirements = (requirementIndex !== undefined
? [spec.requirements[requirementIndex]]
: spec.requirements
).map(req => ({
text: req.text,
scenarios: includeScenarios ? req.scenarios : [],
}));
const metadata = spec.metadata ?? { version: '1.0.0', format: 'openspec' as const };
return {
name: spec.name,
overview: spec.overview,
requirements: filteredRequirements,
metadata,
};
}
/**
* Print the raw markdown content for a spec file without any formatting.
* Raw-first behavior ensures text mode is a passthrough for deterministic output.
*/
function printSpecTextRaw(specPath: string): void {
const content = readFileSync(specPath, 'utf-8');
console.log(content);
}
// Deprecation notice for noun-based commands
specCommand.hook('preAction', () => {
console.error('Warning: The "openspec spec ..." commands are deprecated. Prefer verb-first commands (e.g., "openspec show", "openspec validate --specs").');
});
specCommand
.command('show <spec-id>')
.command('show [spec-id]')
.description('Display a specific specification')
.option('--json', 'Output as JSON')
.option('--requirements', 'JSON only: Show only requirements (exclude scenarios)')
.option('--no-scenarios', 'JSON only: Exclude scenario content')
.option('-r, --requirement <id>', 'JSON only: Show specific requirement by ID (1-based)')
.action((specId: string, options: ShowOptions) => {
.option('--no-interactive', 'Disable interactive prompts')
.action(async (specId: string | undefined, options: ShowOptions & { noInteractive?: boolean }) => {
try {
const specPath = join(SPECS_DIR, specId, 'spec.md');
if (!existsSync(specPath)) {
throw new Error(`Spec '${specId}' not found at openspec/specs/${specId}/spec.md`);
}
if (options.json) {
if (options.requirements && options.requirement) {
throw new Error('Options --requirements and --requirement cannot be used together');
}
const parsed = parseSpecFromFile(specPath, specId);
const filtered = filterSpec(parsed, options);
const output = {
id: specId,
title: parsed.name,
overview: parsed.overview,
requirementCount: filtered.requirements.length,
requirements: filtered.requirements,
metadata: parsed.metadata ?? { version: '1.0.0', format: 'openspec' as const },
};
console.log(JSON.stringify(output, null, 2));
} else {
// raw-first text: print raw file
printSpecTextRaw(specPath);
}
const cmd = new SpecCommand();
await cmd.show(specId, options as any);
} catch (error) {
console.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
process.exitCode = 1;
@@ -166,12 +196,26 @@ export function registerSpecCommand(rootProgram: typeof program) {
});
specCommand
.command('validate <spec-id>')
.command('validate [spec-id]')
.description('Validate a specification structure')
.option('--strict', 'Enable strict validation mode')
.option('--json', 'Output validation report as JSON')
.action(async (specId: string, options: { strict?: boolean; json?: boolean }) => {
.option('--no-interactive', 'Disable interactive prompts')
.action(async (specId: string | undefined, options: { strict?: boolean; json?: boolean; noInteractive?: boolean }) => {
try {
if (!specId) {
const canPrompt = isInteractive(options?.noInteractive);
const specIds = await getSpecIds();
if (canPrompt && specIds.length > 0) {
specId = await select({
message: 'Select a spec to validate',
choices: specIds.map(id => ({ name: id, value: id })),
});
} else {
throw new Error('Missing required argument <spec-id>');
}
}
const specPath = join(SPECS_DIR, specId, 'spec.md');
if (!existsSync(specPath)) {
+305
View File
@@ -0,0 +1,305 @@
import { select } from '@inquirer/prompts';
import ora from 'ora';
import path from 'path';
import { Validator } from '../core/validation/validator.js';
import { isInteractive } from '../utils/interactive.js';
import { getActiveChangeIds, getSpecIds } from '../utils/item-discovery.js';
import { nearestMatches } from '../utils/match.js';
type ItemType = 'change' | 'spec';
interface ExecuteOptions {
all?: boolean;
changes?: boolean;
specs?: boolean;
type?: string;
strict?: boolean;
json?: boolean;
noInteractive?: boolean;
concurrency?: string;
}
interface BulkItemResult {
id: string;
type: ItemType;
valid: boolean;
issues: { level: 'ERROR' | 'WARNING' | 'INFO'; path: string; message: string }[];
durationMs: number;
}
export class ValidateCommand {
async execute(itemName: string | undefined, options: ExecuteOptions = {}): Promise<void> {
const interactive = isInteractive(options.noInteractive);
// Handle bulk flags first
if (options.all || options.changes || options.specs) {
await this.runBulkValidation({
changes: !!options.all || !!options.changes,
specs: !!options.all || !!options.specs,
}, { strict: !!options.strict, json: !!options.json, concurrency: options.concurrency });
return;
}
// No item and no flags
if (!itemName) {
if (interactive) {
await this.runInteractiveSelector({ strict: !!options.strict, json: !!options.json, concurrency: options.concurrency });
return;
}
this.printNonInteractiveHint();
process.exitCode = 1;
return;
}
// Direct item validation with type detection or override
const typeOverride = this.normalizeType(options.type);
await this.validateDirectItem(itemName, { typeOverride, strict: !!options.strict, json: !!options.json });
}
private normalizeType(value?: string): ItemType | undefined {
if (!value) return undefined;
const v = value.toLowerCase();
if (v === 'change' || v === 'spec') return v;
return undefined;
}
private async runInteractiveSelector(opts: { strict: boolean; json: boolean; concurrency?: string }): Promise<void> {
const choice = await select({
message: 'What would you like to validate?',
choices: [
{ name: 'All (changes + specs)', value: 'all' },
{ name: 'All changes', value: 'changes' },
{ name: 'All specs', value: 'specs' },
{ name: 'Pick a specific change or spec', value: 'one' },
],
});
if (choice === 'all') return this.runBulkValidation({ changes: true, specs: true }, opts);
if (choice === 'changes') return this.runBulkValidation({ changes: true, specs: false }, opts);
if (choice === 'specs') return this.runBulkValidation({ changes: false, specs: true }, opts);
// one
const [changes, specs] = await Promise.all([getActiveChangeIds(), getSpecIds()]);
const items: { name: string; value: { type: ItemType; id: string } }[] = [];
items.push(...changes.map(id => ({ name: `change/${id}`, value: { type: 'change' as const, id } })));
items.push(...specs.map(id => ({ name: `spec/${id}`, value: { type: 'spec' as const, id } })));
if (items.length === 0) {
console.error('No items found to validate.');
process.exitCode = 1;
return;
}
const picked = await select<{ type: ItemType; id: string }>({ message: 'Pick an item', choices: items });
await this.validateByType(picked.type, picked.id, opts);
}
private printNonInteractiveHint(): void {
console.error('Nothing to validate. Try one of:');
console.error(' openspec validate --all');
console.error(' openspec validate --changes');
console.error(' openspec validate --specs');
console.error(' openspec validate <item-name>');
console.error('Or run in an interactive terminal.');
}
private async validateDirectItem(itemName: string, opts: { typeOverride?: ItemType; strict: boolean; json: boolean }): Promise<void> {
const [changes, specs] = await Promise.all([getActiveChangeIds(), getSpecIds()]);
const isChange = changes.includes(itemName);
const isSpec = specs.includes(itemName);
const type = opts.typeOverride ?? (isChange ? 'change' : isSpec ? 'spec' : undefined);
if (!type) {
console.error(`Unknown item '${itemName}'`);
const suggestions = nearestMatches(itemName, [...changes, ...specs]);
if (suggestions.length) console.error(`Did you mean: ${suggestions.join(', ')}?`);
process.exitCode = 1;
return;
}
if (!opts.typeOverride && isChange && isSpec) {
console.error(`Ambiguous item '${itemName}' matches both a change and a spec.`);
console.error('Pass --type change|spec, or use: openspec change validate / openspec spec validate');
process.exitCode = 1;
return;
}
await this.validateByType(type, itemName, opts);
}
private async validateByType(type: ItemType, id: string, opts: { strict: boolean; json: boolean }): Promise<void> {
const validator = new Validator(opts.strict);
if (type === 'change') {
const changeDir = path.join(process.cwd(), 'openspec', 'changes', id);
const start = Date.now();
const report = await validator.validateChangeDeltaSpecs(changeDir);
const durationMs = Date.now() - start;
this.printReport('change', id, report, durationMs, opts.json);
// Non-zero exit if invalid (keeps enriched output test semantics)
process.exitCode = report.valid ? 0 : 1;
return;
}
const file = path.join(process.cwd(), 'openspec', 'specs', id, 'spec.md');
const start = Date.now();
const report = await validator.validateSpec(file);
const durationMs = Date.now() - start;
this.printReport('spec', id, report, durationMs, opts.json);
process.exitCode = report.valid ? 0 : 1;
}
private printReport(type: ItemType, id: string, report: { valid: boolean; issues: any[] }, durationMs: number, json: boolean): void {
if (json) {
const out = { items: [{ id, type, valid: report.valid, issues: report.issues, durationMs }], summary: { totals: { items: 1, passed: report.valid ? 1 : 0, failed: report.valid ? 0 : 1 }, byType: { [type]: { items: 1, passed: report.valid ? 1 : 0, failed: report.valid ? 0 : 1 } } }, version: '1.0' };
console.log(JSON.stringify(out, null, 2));
return;
}
if (report.valid) {
console.log(`${type === 'change' ? 'Change' : 'Specification'} '${id}' is valid`);
} else {
console.error(`${type === 'change' ? 'Change' : 'Specification'} '${id}' has issues`);
for (const issue of report.issues) {
const label = issue.level === 'ERROR' ? 'ERROR' : issue.level;
const prefix = issue.level === 'ERROR' ? '✗' : issue.level === 'WARNING' ? '⚠' : 'ℹ';
console.error(`${prefix} [${label}] ${issue.path}: ${issue.message}`);
}
this.printNextSteps(type);
}
}
private printNextSteps(type: ItemType): void {
const bullets: string[] = [];
if (type === 'change') {
bullets.push('- Ensure change has deltas in specs/: use headers ## ADDED/MODIFIED/REMOVED/RENAMED Requirements');
bullets.push('- Each requirement MUST include at least one #### Scenario: block');
bullets.push('- Debug parsed deltas: openspec change show <id> --json --deltas-only');
} else {
bullets.push('- Ensure spec includes ## Purpose and ## Requirements sections');
bullets.push('- Each requirement MUST include at least one #### Scenario: block');
bullets.push('- Re-run with --json to see structured report');
}
console.error('Next steps:');
bullets.forEach(b => console.error(` ${b}`));
}
private async runBulkValidation(scope: { changes: boolean; specs: boolean }, opts: { strict: boolean; json: boolean; concurrency?: string }): Promise<void> {
const spinner = !opts.json ? ora('Validating...').start() : undefined;
const [changeIds, specIds] = await Promise.all([
scope.changes ? getActiveChangeIds() : Promise.resolve<string[]>([]),
scope.specs ? getSpecIds() : Promise.resolve<string[]>([]),
]);
const DEFAULT_CONCURRENCY = 6;
const maxSuggestions = 5; // used by nearestMatches
const concurrency = normalizeConcurrency(opts.concurrency) ?? normalizeConcurrency(process.env.OPENSPEC_CONCURRENCY) ?? DEFAULT_CONCURRENCY;
const validator = new Validator(opts.strict);
const queue: Array<() => Promise<BulkItemResult>> = [];
for (const id of changeIds) {
queue.push(async () => {
const start = Date.now();
const changeDir = path.join(process.cwd(), 'openspec', 'changes', id);
const report = await validator.validateChangeDeltaSpecs(changeDir);
const durationMs = Date.now() - start;
return { id, type: 'change' as const, valid: report.valid, issues: report.issues, durationMs };
});
}
for (const id of specIds) {
queue.push(async () => {
const start = Date.now();
const file = path.join(process.cwd(), 'openspec', 'specs', id, 'spec.md');
const report = await validator.validateSpec(file);
const durationMs = Date.now() - start;
return { id, type: 'spec' as const, valid: report.valid, issues: report.issues, durationMs };
});
}
const results: BulkItemResult[] = [];
let index = 0;
let running = 0;
let passed = 0;
let failed = 0;
await new Promise<void>((resolve) => {
const next = () => {
while (running < concurrency && index < queue.length) {
const currentIndex = index++;
const task = queue[currentIndex];
running++;
if (spinner) spinner.text = `Validating (${currentIndex + 1}/${queue.length})...`;
task()
.then(res => {
results.push(res);
if (res.valid) passed++; else failed++;
})
.catch((error: any) => {
const message = error?.message || 'Unknown error';
const res: BulkItemResult = { id: getPlannedId(currentIndex, changeIds, specIds) ?? 'unknown', type: getPlannedType(currentIndex, changeIds, specIds) ?? 'change', valid: false, issues: [{ level: 'ERROR', path: 'file', message }], durationMs: 0 };
results.push(res);
failed++;
})
.finally(() => {
running--;
if (index >= queue.length && running === 0) resolve();
else next();
});
}
};
next();
});
spinner?.stop();
results.sort((a, b) => a.id.localeCompare(b.id));
const summary = {
totals: { items: results.length, passed, failed },
byType: {
...(scope.changes ? { change: summarizeType(results, 'change') } : {}),
...(scope.specs ? { spec: summarizeType(results, 'spec') } : {}),
},
} as const;
if (opts.json) {
const out = { items: results, summary, version: '1.0' };
console.log(JSON.stringify(out, null, 2));
} else {
for (const res of results) {
if (res.valid) console.log(`✓ ${res.type}/${res.id}`);
else console.error(`✗ ${res.type}/${res.id}`);
}
console.log(`Totals: ${summary.totals.passed} passed, ${summary.totals.failed} failed (${summary.totals.items} items)`);
}
process.exitCode = failed > 0 ? 1 : 0;
}
}
function summarizeType(results: BulkItemResult[], type: ItemType) {
const filtered = results.filter(r => r.type === type);
const items = filtered.length;
const passed = filtered.filter(r => r.valid).length;
const failed = items - passed;
return { items, passed, failed };
}
function normalizeConcurrency(value?: string): number | undefined {
if (!value) return undefined;
const n = parseInt(value, 10);
if (Number.isNaN(n) || n <= 0) return undefined;
return n;
}
function getPlannedId(index: number, changeIds: string[], specIds: string[]): string | undefined {
const totalChanges = changeIds.length;
if (index < totalChanges) return changeIds[index];
const specIndex = index - totalChanges;
return specIds[specIndex];
}
function getPlannedType(index: number, changeIds: string[], specIds: string[]): ItemType | undefined {
const totalChanges = changeIds.length;
if (index < totalChanges) return 'change';
const specIndex = index - totalChanges;
if (specIndex >= 0 && specIndex < specIds.length) return 'spec';
return undefined;
}
+51 -41
View File
@@ -59,16 +59,48 @@ export class ArchiveCommand {
const validator = new Validator();
let hasValidationErrors = false;
// Validate change.md file
const changeFile = path.join(changeDir, 'change.md');
// Validate proposal.md (non-blocking unless strict mode desired in future)
const changeFile = path.join(changeDir, 'proposal.md');
try {
await fs.access(changeFile);
const changeReport = await validator.validateChange(changeFile);
// Proposal validation is informative only (do not block archive)
if (!changeReport.valid) {
hasValidationErrors = true;
console.log(chalk.red(`\nValidation errors in change.md:`));
console.log(chalk.yellow(`\nProposal warnings in proposal.md (non-blocking):`));
for (const issue of changeReport.issues) {
const symbol = issue.level === 'ERROR' ? '⚠' : (issue.level === 'WARNING' ? '⚠' : 'ℹ');
console.log(chalk.yellow(` ${symbol} ${issue.message}`));
}
}
} catch {
// Change file doesn't exist, skip validation
}
// Validate delta-formatted spec files under the change directory if present
const changeSpecsDir = path.join(changeDir, 'specs');
let hasDeltaSpecs = false;
try {
const candidates = await fs.readdir(changeSpecsDir, { withFileTypes: true });
for (const c of candidates) {
if (c.isDirectory()) {
try {
const candidatePath = path.join(changeSpecsDir, c.name, 'spec.md');
await fs.access(candidatePath);
const content = await fs.readFile(candidatePath, 'utf-8');
if (/^##\s+(ADDED|MODIFIED|REMOVED|RENAMED)\s+Requirements/m.test(content)) {
hasDeltaSpecs = true;
break;
}
} catch {}
}
}
} catch {}
if (hasDeltaSpecs) {
const deltaReport = await validator.validateChangeDeltaSpecs(changeDir);
if (!deltaReport.valid) {
hasValidationErrors = true;
console.log(chalk.red(`\nValidation errors in change delta specs:`));
for (const issue of deltaReport.issues) {
if (issue.level === 'ERROR') {
console.log(chalk.red(` ✗ ${issue.message}`));
} else if (issue.level === 'WARNING') {
@@ -76,41 +108,6 @@ export class ArchiveCommand {
}
}
}
} catch {
// Change file doesn't exist, skip validation
}
// Validate spec files
const changeSpecsDir = path.join(changeDir, 'specs');
try {
const entries = await fs.readdir(changeSpecsDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) {
const specFile = path.join(changeSpecsDir, entry.name, 'spec.md');
try {
await fs.access(specFile);
const report = await validator.validateSpec(specFile);
if (!report.valid) {
hasValidationErrors = true;
console.log(chalk.red(`\nValidation errors in ${entry.name}/spec.md:`));
for (const issue of report.issues) {
if (issue.level === 'ERROR') {
console.log(chalk.red(` ✗ ${issue.message}`));
} else if (issue.level === 'WARNING') {
console.log(chalk.yellow(` ⚠ ${issue.message}`));
}
}
}
} catch {
// Spec file doesn't exist, skip validation
}
}
}
} catch {
// No specs directory, skip validation
}
if (hasValidationErrors) {
@@ -200,9 +197,22 @@ export class ArchiveCommand {
return;
}
// All validations passed; write files and display counts
// All validations passed; pre-validate rebuilt full spec and then write files and display counts
let totals = { added: 0, modified: 0, removed: 0, renamed: 0 };
for (const p of prepared) {
const specName = path.basename(path.dirname(p.update.target));
if (!options.noValidate) {
const report = await new Validator().validateSpecContent(specName, p.rebuilt);
if (!report.valid) {
console.log(chalk.red(`\nValidation errors in rebuilt spec for ${specName} (will not write changes):`));
for (const issue of report.issues) {
if (issue.level === 'ERROR') console.log(chalk.red(` ✗ ${issue.message}`));
else if (issue.level === 'WARNING') console.log(chalk.yellow(` ⚠ ${issue.message}`));
}
console.log('Aborted. No files were changed.');
return;
}
}
await this.writeUpdatedSpec(p.update, p.rebuilt, p.counts);
totals.added += p.counts.added;
totals.modified += p.counts.modified;
-227
View File
@@ -1,227 +0,0 @@
import { promises as fs } from 'fs';
import path from 'path';
import chalk from 'chalk';
import { diffStringsUnified } from 'jest-diff';
import { select } from '@inquirer/prompts';
import { Validator } from './validation/validator.js';
// Constants
const ARCHIVE_DIR = 'archive';
const MARKDOWN_EXT = '.md';
const OPENSPEC_DIR = 'openspec';
const CHANGES_DIR = 'changes';
const SPECS_DIR = 'specs';
export class DiffCommand {
private filesChanged: number = 0;
private linesAdded: number = 0;
private linesRemoved: number = 0;
async execute(changeName?: string): Promise<void> {
const changesDir = path.join(process.cwd(), OPENSPEC_DIR, CHANGES_DIR);
try {
await fs.access(changesDir);
} catch {
throw new Error('No OpenSpec changes directory found');
}
if (!changeName) {
changeName = await this.selectChange(changesDir);
if (!changeName) return;
}
const changeDir = path.join(changesDir, changeName);
try {
await fs.access(changeDir);
} catch {
throw new Error(`Change '${changeName}' not found`);
}
const changeSpecsDir = path.join(changeDir, SPECS_DIR);
try {
await fs.access(changeSpecsDir);
} catch {
console.log(`No spec changes found for '${changeName}'`);
return;
}
// Validate specs and show warnings (non-blocking)
const validator = new Validator();
let hasWarnings = false;
try {
const entries = await fs.readdir(changeSpecsDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) {
const specFile = path.join(changeSpecsDir, entry.name, 'spec.md');
try {
await fs.access(specFile);
const report = await validator.validateSpec(specFile);
if (report.issues.length > 0) {
const warnings = report.issues.filter(i => i.level === 'WARNING');
const errors = report.issues.filter(i => i.level === 'ERROR');
if (errors.length > 0 || warnings.length > 0) {
if (!hasWarnings) {
console.log(chalk.yellow('\n⚠️ Validation warnings found:'));
hasWarnings = true;
}
console.log(chalk.yellow(`\n ${entry.name}/spec.md:`));
for (const issue of errors) {
console.log(chalk.red(` ✗ ${issue.message}`));
}
for (const issue of warnings) {
console.log(chalk.yellow(` ⚠ ${issue.message}`));
}
}
}
} catch {
// Spec file doesn't exist, skip validation
}
}
}
if (hasWarnings) {
console.log(chalk.yellow('\nConsider fixing these issues before archiving.\n'));
}
} catch {
// No specs directory, skip validation
}
// Reset counters
this.filesChanged = 0;
this.linesAdded = 0;
this.linesRemoved = 0;
await this.showDiffs(changeSpecsDir);
// Show summary
if (this.filesChanged > 0) {
console.log(chalk.bold(`\n📊 Summary: ${this.filesChanged} file(s) changed, ${chalk.green(`+${this.linesAdded}`)} ${chalk.red(`-${this.linesRemoved}`)}`));
}
}
private async selectChange(changesDir: string): Promise<string | undefined> {
const entries = await fs.readdir(changesDir, { withFileTypes: true });
const changes = entries
.filter(entry => entry.isDirectory() && entry.name !== ARCHIVE_DIR)
.map(entry => entry.name);
if (changes.length === 0) {
console.log('No changes found');
return undefined;
}
console.log('Available changes:');
const choices = changes.map((name) => ({
name: name,
value: name
}));
const answer = await select({
message: 'Select a change',
choices
});
return answer as string;
}
private async showDiffs(changeSpecsDir: string): Promise<void> {
const currentSpecsDir = path.join(process.cwd(), OPENSPEC_DIR, SPECS_DIR);
await this.walkAndDiff(changeSpecsDir, currentSpecsDir, '');
}
private async walkAndDiff(changeDir: string, currentDir: string, relativePath: string): Promise<void> {
const entries = await fs.readdir(path.join(changeDir, relativePath), { withFileTypes: true });
for (const entry of entries) {
const entryPath = path.join(relativePath, entry.name);
if (entry.isDirectory()) {
await this.walkAndDiff(changeDir, currentDir, entryPath);
} else if (entry.isFile() && entry.name.endsWith(MARKDOWN_EXT)) {
await this.diffFile(
path.join(changeDir, entryPath),
path.join(currentDir, entryPath),
entryPath
);
}
}
}
private async diffFile(changePath: string, currentPath: string, displayPath: string): Promise<void> {
let changeContent = '';
let currentContent = '';
let isNewFile = false;
let isDeleted = false;
try {
changeContent = await fs.readFile(changePath, 'utf-8');
} catch {
changeContent = '';
}
try {
currentContent = await fs.readFile(currentPath, 'utf-8');
} catch {
currentContent = '';
isNewFile = true;
}
if (changeContent === currentContent) {
return;
}
if (changeContent === '' && currentContent !== '') {
isDeleted = true;
}
// Enhanced header with file status
console.log(chalk.bold.cyan(`\n${'═'.repeat(60)}`));
console.log(chalk.bold.cyan(`📄 ${displayPath}`));
if (isNewFile) {
console.log(chalk.green(` Status: NEW FILE`));
} else if (isDeleted) {
console.log(chalk.red(` Status: DELETED`));
} else {
console.log(chalk.yellow(` Status: MODIFIED`));
}
// Use jest-diff for the actual diff with custom options
const diffOptions = {
aAnnotation: 'Current',
bAnnotation: 'Proposed',
aColor: chalk.red,
bColor: chalk.green,
commonColor: chalk.gray,
contextLines: 3,
expand: false,
includeChangeCounts: true,
};
const diff = diffStringsUnified(currentContent, changeContent, diffOptions);
// Count lines for statistics (approximate)
const addedLines = (diff.match(/^\+[^+]/gm) || []).length;
const removedLines = (diff.match(/^-[^-]/gm) || []).length;
console.log(chalk.gray(` Lines: ${chalk.green(`+${addedLines}`)} ${chalk.red(`-${removedLines}`)}`));
console.log(chalk.bold.cyan(`${'─'.repeat(60)}\n`));
// Display the diff
console.log(diff);
// Update counters
this.filesChanged++;
this.linesAdded += addedLines;
this.linesRemoved += removedLines;
}
}
+82 -38
View File
@@ -1,6 +1,9 @@
import { promises as fs } from 'fs';
import path from 'path';
import { getTaskProgressForChange, formatTaskStatus } from '../utils/task-progress.js';
import { readFileSync } from 'fs';
import { join } from 'path';
import { MarkdownParser } from './parsers/markdown-parser.js';
interface ChangeInfo {
name: string;
@@ -9,52 +12,93 @@ interface ChangeInfo {
}
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.");
}
async execute(targetPath: string = '.', mode: 'changes' | 'specs' = 'changes'): Promise<void> {
if (mode === 'changes') {
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);
// 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.');
if (changeDirs.length === 0) {
console.log('No active changes found.');
return;
}
// Collect information about each change
const changes: ChangeInfo[] = [];
for (const changeDir of changeDirs) {
const progress = await getTaskProgressForChange(changesDir, changeDir);
changes.push({
name: changeDir,
completedTasks: progress.completed,
totalTasks: progress.total
});
}
// Sort alphabetically by name
changes.sort((a, b) => a.name.localeCompare(b.name));
// Display results
console.log('Changes:');
const padding = ' ';
const nameWidth = Math.max(...changes.map(c => c.name.length));
for (const change of changes) {
const paddedName = change.name.padEnd(nameWidth);
const status = formatTaskStatus({ total: change.totalTasks, completed: change.completedTasks });
console.log(`${padding}${paddedName} ${status}`);
}
return;
}
// Collect information about each change
const changes: ChangeInfo[] = [];
for (const changeDir of changeDirs) {
const progress = await getTaskProgressForChange(changesDir, changeDir);
changes.push({
name: changeDir,
completedTasks: progress.completed,
totalTasks: progress.total
});
// specs mode
const specsDir = path.join(targetPath, 'openspec', 'specs');
try {
await fs.access(specsDir);
} catch {
console.log('No specs found.');
return;
}
// Sort alphabetically by name
changes.sort((a, b) => a.name.localeCompare(b.name));
const entries = await fs.readdir(specsDir, { withFileTypes: true });
const specDirs = entries.filter(e => e.isDirectory()).map(e => e.name);
if (specDirs.length === 0) {
console.log('No specs found.');
return;
}
// Display results
console.log('Changes:');
for (const change of changes) {
const padding = ' ';
const nameWidth = Math.max(...changes.map(c => c.name.length));
const paddedName = change.name.padEnd(nameWidth);
const status = formatTaskStatus({ total: change.totalTasks, completed: change.completedTasks });
console.log(`${padding}${paddedName} ${status}`);
type SpecInfo = { id: string; requirementCount: number };
const specs: SpecInfo[] = [];
for (const id of specDirs) {
const specPath = join(specsDir, id, 'spec.md');
try {
const content = readFileSync(specPath, 'utf-8');
const parser = new MarkdownParser(content);
const spec = parser.parseSpec(id);
specs.push({ id, requirementCount: spec.requirements.length });
} catch {
// If spec cannot be read or parsed, include with 0 count
specs.push({ id, requirementCount: 0 });
}
}
specs.sort((a, b) => a.id.localeCompare(b.id));
console.log('Specs:');
const padding = ' ';
const nameWidth = Math.max(...specs.map(s => s.id.length));
for (const spec of specs) {
const padded = spec.id.padEnd(nameWidth);
console.log(`${padding}${padded} requirements ${spec.requirementCount}`);
}
}
}
+4 -1
View File
@@ -94,7 +94,8 @@ export class ChangeParser extends MarkdownParser {
spec: specName,
operation: 'ADDED' as DeltaOperation,
description: `Add requirement: ${req.text}`,
// Use plural form to satisfy validators that expect an array
// Provide both single and plural forms for compatibility
requirement: req,
requirements: [req],
});
});
@@ -109,6 +110,7 @@ export class ChangeParser extends MarkdownParser {
spec: specName,
operation: 'MODIFIED' as DeltaOperation,
description: `Modify requirement: ${req.text}`,
requirement: req,
requirements: [req],
});
});
@@ -123,6 +125,7 @@ export class ChangeParser extends MarkdownParser {
spec: specName,
operation: 'REMOVED' as DeltaOperation,
description: `Remove requirement: ${req.text}`,
requirement: req,
requirements: [req],
});
});
+83 -15
View File
@@ -1,26 +1,94 @@
export const claudeTemplate = `# OpenSpec Project
This document provides instructions for AI coding assistants on how to use OpenSpec conventions for spec-driven development. Follow these rules precisely when working on OpenSpec-enabled projects.
This project uses OpenSpec for spec-driven development. Specifications are the source of truth.
See @openspec/README.md for detailed conventions and guidelines.
## Three-Stage Workflow
### Stage 1: Creating Changes
Create proposal for: features, breaking changes, architecture changes
Skip proposal for: bug fixes, typos, non-breaking updates
### Stage 2: Implementing Changes
1. Read proposal.md to understand the change
2. Read design.md if it exists for technical context
3. Read tasks.md for implementation checklist
4. Complete tasks one by one
5. Mark each task complete immediately: \`- [x]\`
### Stage 3: Archiving
After deployment, use \`openspec archive [change]\` (add \`--skip-specs\` for tooling-only changes)
## Before Any Task
**Always:**
- Check existing specs: \`openspec list --specs\`
- Check active changes: \`openspec list\`
- Read relevant specs before creating new ones
- Prefer modifying existing specs over creating duplicates
## CLI Quick Reference
\`\`\`bash
# Essential
openspec list # Active changes
openspec list --specs # Existing specifications
openspec show [item] # View details
openspec validate --strict # Validate thoroughly
openspec archive [change] # Archive after deployment
# Interactive
openspec show # Prompts for selection
openspec validate # Bulk validation
# Debugging
openspec show [change] --json --deltas-only
\`\`\`
## Creating Changes
1. **Directory:** \`changes/[descriptive-name]/\`
2. **Files:**
- \`proposal.md\` - Why, what, impact
- \`tasks.md\` - Implementation checklist
- \`specs/[capability]/spec.md\` - Delta changes (ADDED/MODIFIED/REMOVED)
## Critical: Scenario Format
**CORRECT:**
\`\`\`markdown
#### Scenario: User login
- **WHEN** valid credentials
- **THEN** return token
\`\`\`
**WRONG:** Using bullets (- **Scenario**), bold (**Scenario:**), or ### headers
Every requirement MUST have scenarios using \`#### Scenario:\` format.
## Complexity Management
**Default to minimal solutions:**
- Propose <100 lines of new code for features
- Prefer single-file implementations until proven insufficient
- Avoid frameworks, abstractions, and optimizations without clear justification
- Choose boring, well-understood patterns over novel approaches
**Default to minimal:**
- <100 lines of new code
- Single-file implementations
- No frameworks without justification
- Boring, proven patterns
**Question requests for complexity:**
- Caching? → Ask for performance data and targets
- New framework? → Suggest plain code first
- Extra layers? → Start with the thinnest viable design
**Only add complexity with:**
- Performance data showing need
- Concrete scale requirements (>1000 users)
- Multiple proven use cases
**Justify complexity with data:**
- Performance metrics showing current solution is too slow
- Concrete scale requirements (e.g., >1000 users, >100MB data)
- Multiple proven use cases requiring an abstraction
## Troubleshooting
**"Change must have at least one delta"**
- Check \`changes/[name]/specs/\` exists
- Verify operation prefixes (## ADDED Requirements)
**"Requirement must have at least one scenario"**
- Use \`#### Scenario:\` format (4 hashtags)
- Don't use bullets or bold
**Debug:** \`openspec show [change] --json --deltas-only\`
`;
+10
View File
@@ -35,4 +35,14 @@ export const VALIDATION_MESSAGES = {
REQUIREMENT_TOO_LONG: `Requirement text is very long (>${MAX_REQUIREMENT_TEXT_LENGTH} characters). Consider breaking it down.`,
DELTA_DESCRIPTION_TOO_BRIEF: 'Delta description is too brief',
DELTA_MISSING_REQUIREMENTS: 'Delta should include requirements',
// Guidance snippets (appended to primary messages for remediation)
GUIDE_NO_DELTAS:
'No deltas found. Ensure your change has a specs/ directory with .md files using delta headers (## ADDED/MODIFIED/REMOVED/RENAMED Requirements) and that each requirement includes at least one "#### Scenario:" block. Tip: run "openspec change show <change-id> --json --deltas-only" to inspect parsed deltas.',
GUIDE_MISSING_SPEC_SECTIONS:
'Missing required sections. Expected headers: "## Purpose" and "## Requirements". Example:\n## Purpose\n[brief purpose]\n\n## Requirements\n### Requirement: Clear requirement statement\nUsers SHALL ...\n\n#### Scenario: Descriptive name\n- **WHEN** ...\n- **THEN** ...',
GUIDE_MISSING_CHANGE_SECTIONS:
'Missing required sections. Expected headers: "## Why" and "## What Changes". Ensure deltas are documented in specs/ using delta headers.',
GUIDE_SCENARIO_FORMAT:
'Scenarios must use level-4 headers. Convert bullet lists into:\n#### Scenario: Short name\n- **WHEN** ...\n- **THEN** ...\n- **AND** ...',
} as const;
+222 -13
View File
@@ -1,5 +1,5 @@
import { z, ZodError } from 'zod';
import { readFileSync } from 'fs';
import { readFileSync, promises as fs } from 'fs';
import path from 'path';
import { SpecSchema, ChangeSchema, Spec, Change } from '../schemas/index.js';
import { MarkdownParser } from '../parsers/markdown-parser.js';
@@ -10,6 +10,7 @@ import {
MAX_REQUIREMENT_TEXT_LENGTH,
VALIDATION_MESSAGES
} from './constants.js';
import { parseDeltaSpec, normalizeRequirementName } from '../parsers/requirement-blocks.js';
export class Validator {
private strictMode: boolean;
@@ -20,11 +21,10 @@ export class Validator {
async validateSpec(filePath: string): Promise<ValidationReport> {
const issues: ValidationIssue[] = [];
const specName = this.extractNameFromPath(filePath);
try {
const content = readFileSync(filePath, 'utf-8');
const parser = new MarkdownParser(content);
const specName = this.extractNameFromPath(filePath);
const spec = parser.parseSpec(specName);
@@ -37,22 +37,44 @@ export class Validator {
issues.push(...this.applySpecRules(spec, content));
} catch (error) {
const baseMessage = error instanceof Error ? error.message : 'Unknown error';
const enriched = this.enrichTopLevelError(specName, baseMessage);
issues.push({
level: 'ERROR',
path: 'file',
message: error instanceof Error ? error.message : 'Unknown error',
message: enriched,
});
}
return this.createReport(issues);
}
/**
* Validate spec content from a string (used for pre-write validation of rebuilt specs)
*/
async validateSpecContent(specName: string, content: string): Promise<ValidationReport> {
const issues: ValidationIssue[] = [];
try {
const parser = new MarkdownParser(content);
const spec = parser.parseSpec(specName);
const result = SpecSchema.safeParse(spec);
if (!result.success) {
issues.push(...this.convertZodErrors(result.error));
}
issues.push(...this.applySpecRules(spec, content));
} catch (error) {
const baseMessage = error instanceof Error ? error.message : 'Unknown error';
const enriched = this.enrichTopLevelError(specName, baseMessage);
issues.push({ level: 'ERROR', path: 'file', message: enriched });
}
return this.createReport(issues);
}
async validateChange(filePath: string): Promise<ValidationReport> {
const issues: ValidationIssue[] = [];
const changeName = this.extractNameFromPath(filePath);
try {
const content = readFileSync(filePath, 'utf-8');
const changeName = this.extractNameFromPath(filePath);
const changeDir = path.dirname(filePath);
const parser = new ChangeParser(content, changeDir);
@@ -67,22 +89,172 @@ export class Validator {
issues.push(...this.applyChangeRules(change, content));
} catch (error) {
const baseMessage = error instanceof Error ? error.message : 'Unknown error';
const enriched = this.enrichTopLevelError(changeName, baseMessage);
issues.push({
level: 'ERROR',
path: 'file',
message: error instanceof Error ? error.message : 'Unknown error',
message: enriched,
});
}
return this.createReport(issues);
}
/**
* Validate delta-formatted spec files under a change directory.
* Enforces:
* - At least one delta across all files
* - ADDED/MODIFIED: each requirement has SHALL/MUST and at least one scenario
* - REMOVED: names only; no scenario/description required
* - RENAMED: pairs well-formed
* - No duplicates within sections; no cross-section conflicts per spec
*/
async validateChangeDeltaSpecs(changeDir: string): Promise<ValidationReport> {
const issues: ValidationIssue[] = [];
const specsDir = path.join(changeDir, 'specs');
let totalDeltas = 0;
try {
const entries = await fs.readdir(specsDir, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const specName = entry.name;
const specFile = path.join(specsDir, specName, 'spec.md');
let content: string | undefined;
try {
content = await fs.readFile(specFile, 'utf-8');
} catch {
continue;
}
const plan = parseDeltaSpec(content);
const entryPath = `${specName}/spec.md`;
const addedNames = new Set<string>();
const modifiedNames = new Set<string>();
const removedNames = new Set<string>();
const renamedFrom = new Set<string>();
const renamedTo = new Set<string>();
// Validate ADDED
for (const block of plan.added) {
const key = normalizeRequirementName(block.name);
totalDeltas++;
if (addedNames.has(key)) {
issues.push({ level: 'ERROR', path: entryPath, message: `Duplicate requirement in ADDED: "${block.name}"` });
} else {
addedNames.add(key);
}
const requirementText = this.extractRequirementText(block.raw);
if (!requirementText) {
issues.push({ level: 'ERROR', path: entryPath, message: `ADDED "${block.name}" is missing requirement text` });
} else if (!this.containsShallOrMust(requirementText)) {
issues.push({ level: 'ERROR', path: entryPath, message: `ADDED "${block.name}" must contain SHALL or MUST` });
}
const scenarioCount = this.countScenarios(block.raw);
if (scenarioCount < 1) {
issues.push({ level: 'ERROR', path: entryPath, message: `ADDED "${block.name}" must include at least one scenario` });
}
}
// Validate MODIFIED
for (const block of plan.modified) {
const key = normalizeRequirementName(block.name);
totalDeltas++;
if (modifiedNames.has(key)) {
issues.push({ level: 'ERROR', path: entryPath, message: `Duplicate requirement in MODIFIED: "${block.name}"` });
} else {
modifiedNames.add(key);
}
const requirementText = this.extractRequirementText(block.raw);
if (!requirementText) {
issues.push({ level: 'ERROR', path: entryPath, message: `MODIFIED "${block.name}" is missing requirement text` });
} else if (!this.containsShallOrMust(requirementText)) {
issues.push({ level: 'ERROR', path: entryPath, message: `MODIFIED "${block.name}" must contain SHALL or MUST` });
}
const scenarioCount = this.countScenarios(block.raw);
if (scenarioCount < 1) {
issues.push({ level: 'ERROR', path: entryPath, message: `MODIFIED "${block.name}" must include at least one scenario` });
}
}
// Validate REMOVED (names only)
for (const name of plan.removed) {
const key = normalizeRequirementName(name);
totalDeltas++;
if (removedNames.has(key)) {
issues.push({ level: 'ERROR', path: entryPath, message: `Duplicate requirement in REMOVED: "${name}"` });
} else {
removedNames.add(key);
}
}
// Validate RENAMED pairs
for (const { from, to } of plan.renamed) {
const fromKey = normalizeRequirementName(from);
const toKey = normalizeRequirementName(to);
totalDeltas++;
if (renamedFrom.has(fromKey)) {
issues.push({ level: 'ERROR', path: entryPath, message: `Duplicate FROM in RENAMED: "${from}"` });
} else {
renamedFrom.add(fromKey);
}
if (renamedTo.has(toKey)) {
issues.push({ level: 'ERROR', path: entryPath, message: `Duplicate TO in RENAMED: "${to}"` });
} else {
renamedTo.add(toKey);
}
}
// Cross-section conflicts (within the same spec file)
for (const n of modifiedNames) {
if (removedNames.has(n)) {
issues.push({ level: 'ERROR', path: entryPath, message: `Requirement present in both MODIFIED and REMOVED: "${n}"` });
}
if (addedNames.has(n)) {
issues.push({ level: 'ERROR', path: entryPath, message: `Requirement present in both MODIFIED and ADDED: "${n}"` });
}
}
for (const n of addedNames) {
if (removedNames.has(n)) {
issues.push({ level: 'ERROR', path: entryPath, message: `Requirement present in both ADDED and REMOVED: "${n}"` });
}
}
for (const { from, to } of plan.renamed) {
const fromKey = normalizeRequirementName(from);
const toKey = normalizeRequirementName(to);
if (modifiedNames.has(fromKey)) {
issues.push({ level: 'ERROR', path: entryPath, message: `MODIFIED references old name from RENAMED. Use new header for "${to}"` });
}
if (addedNames.has(toKey)) {
issues.push({ level: 'ERROR', path: entryPath, message: `RENAMED TO collides with ADDED for "${to}"` });
}
}
}
} catch {
// If no specs dir, treat as no deltas
}
if (totalDeltas === 0) {
issues.push({ level: 'ERROR', path: 'file', message: this.enrichTopLevelError('change', VALIDATION_MESSAGES.CHANGE_NO_DELTAS) });
}
return this.createReport(issues);
}
private convertZodErrors(error: ZodError): ValidationIssue[] {
return error.issues.map(err => ({
level: 'ERROR' as ValidationLevel,
path: err.path.join('.'),
message: err.message,
}));
return error.issues.map(err => {
let message = err.message;
if (message === VALIDATION_MESSAGES.CHANGE_NO_DELTAS) {
message = `${message}. ${VALIDATION_MESSAGES.GUIDE_NO_DELTAS}`;
}
return {
level: 'ERROR' as ValidationLevel,
path: err.path.join('.'),
message,
};
});
}
private applySpecRules(spec: Spec, content: string): ValidationIssue[] {
@@ -109,7 +281,7 @@ export class Validator {
issues.push({
level: 'WARNING',
path: `requirements[${index}].scenarios`,
message: 'Requirement has no scenarios',
message: `${VALIDATION_MESSAGES.REQUIREMENT_NO_SCENARIOS}. ${VALIDATION_MESSAGES.GUIDE_SCENARIO_FORMAT}`,
});
}
});
@@ -144,6 +316,20 @@ export class Validator {
return issues;
}
private enrichTopLevelError(itemId: string, baseMessage: string): string {
const msg = baseMessage.trim();
if (msg === VALIDATION_MESSAGES.CHANGE_NO_DELTAS) {
return `${msg}. ${VALIDATION_MESSAGES.GUIDE_NO_DELTAS}`;
}
if (msg.includes('Spec must have a Purpose section') || msg.includes('Spec must have a Requirements section')) {
return `${msg}. ${VALIDATION_MESSAGES.GUIDE_MISSING_SPEC_SECTIONS}`;
}
if (msg.includes('Change must have a Why section') || msg.includes('Change must have a What Changes section')) {
return `${msg}. ${VALIDATION_MESSAGES.GUIDE_MISSING_CHANGE_SECTIONS}`;
}
return msg;
}
private extractNameFromPath(filePath: string): string {
const parts = filePath.split('/');
@@ -184,4 +370,27 @@ export class Validator {
isValid(report: ValidationReport): boolean {
return report.valid;
}
private extractRequirementText(blockRaw: string): string | undefined {
const lines = blockRaw.split('\n');
// Skip header
let i = 1;
const bodyLines: string[] = [];
for (; i < lines.length; i++) {
const line = lines[i];
if (/^####\s+/.test(line)) break; // scenarios start
bodyLines.push(line);
}
const text = bodyLines.join('\n').split('\n').map(l => l.trim()).find(l => l.length > 0);
return text;
}
private containsShallOrMust(text: string): boolean {
return /\b(SHALL|MUST)\b/.test(text);
}
private countScenarios(blockRaw: string): number {
const matches = blockRaw.match(/^####\s+/gm);
return matches ? matches.length : 0;
}
}
+7
View File
@@ -0,0 +1,7 @@
export function isInteractive(noInteractiveFlag?: boolean): boolean {
if (noInteractiveFlag) return false;
if (process.env.OPEN_SPEC_INTERACTIVE === '0') return false;
return !!process.stdin.isTTY;
}
+45
View File
@@ -0,0 +1,45 @@
import { promises as fs } from 'fs';
import path from 'path';
export async function getActiveChangeIds(root: string = process.cwd()): Promise<string[]> {
const changesPath = path.join(root, 'openspec', 'changes');
try {
const entries = await fs.readdir(changesPath, { withFileTypes: true });
const result: string[] = [];
for (const entry of entries) {
if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === 'archive') continue;
const proposalPath = path.join(changesPath, entry.name, 'proposal.md');
try {
await fs.access(proposalPath);
result.push(entry.name);
} catch {
// skip directories without proposal.md
}
}
return result.sort();
} catch {
return [];
}
}
export async function getSpecIds(root: string = process.cwd()): Promise<string[]> {
const specsPath = path.join(root, 'openspec', 'specs');
const result: string[] = [];
try {
const entries = await fs.readdir(specsPath, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
const specFile = path.join(specsPath, entry.name, 'spec.md');
try {
await fs.access(specFile);
result.push(entry.name);
} catch {
// ignore
}
}
} catch {
// ignore
}
return result.sort();
}
+26
View File
@@ -0,0 +1,26 @@
export function nearestMatches(input: string, candidates: string[], max: number = 5): string[] {
const scored = candidates.map(candidate => ({ candidate, distance: levenshtein(input, candidate) }));
scored.sort((a, b) => a.distance - b.distance);
return scored.slice(0, max).map(s => s.candidate);
}
export function levenshtein(a: string, b: string): number {
const m = a.length;
const n = b.length;
const dp: number[][] = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
for (let i = 0; i <= m; i++) dp[i][0] = i;
for (let j = 0; j <= n; j++) dp[0][j] = j;
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
dp[i][j] = Math.min(
dp[i - 1][j] + 1,
dp[i][j - 1] + 1,
dp[i - 1][j - 1] + cost
);
}
}
return dp[m][n];
}
@@ -0,0 +1,45 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { promises as fs } from 'fs';
import path from 'path';
import { execSync } from 'child_process';
describe('change show (interactive behavior)', () => {
const projectRoot = process.cwd();
const testDir = path.join(projectRoot, 'test-change-show-tmp');
const changesDir = path.join(testDir, 'openspec', 'changes');
const bin = path.join(projectRoot, 'bin', 'openspec.js');
beforeEach(async () => {
await fs.mkdir(changesDir, { recursive: true });
const content = `# Change: Demo\n\n## Why\n\n## What Changes\n- x`;
await fs.mkdir(path.join(changesDir, 'demo'), { recursive: true });
await fs.writeFile(path.join(changesDir, 'demo', 'proposal.md'), content, 'utf-8');
});
afterEach(async () => {
await fs.rm(testDir, { recursive: true, force: true });
});
it('prints list hint and exits non-zero when no arg and non-interactive', () => {
const originalCwd = process.cwd();
const originalEnv = { ...process.env };
try {
process.chdir(testDir);
process.env.OPEN_SPEC_INTERACTIVE = '0';
let err: any;
try {
execSync(`node ${bin} change show`, { encoding: 'utf-8' });
} catch (e) { err = e; }
expect(err).toBeDefined();
expect(err.status).not.toBe(0);
expect(err.stderr.toString()).toContain('Available IDs:');
expect(err.stderr.toString()).toContain('openspec change list');
} finally {
process.chdir(originalCwd);
process.env = originalEnv;
}
});
});
@@ -0,0 +1,48 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { promises as fs } from 'fs';
import path from 'path';
import { execSync } from 'child_process';
// Note: We cannot truly simulate TTY prompts in this test runner easily.
// Instead, we verify non-interactive fallback behavior and basic invocation.
describe('change validate (interactive behavior)', () => {
const projectRoot = process.cwd();
const testDir = path.join(projectRoot, 'test-change-validate-tmp');
const changesDir = path.join(testDir, 'openspec', 'changes');
const bin = path.join(projectRoot, 'bin', 'openspec.js');
beforeEach(async () => {
await fs.mkdir(changesDir, { recursive: true });
const content = `# Change: Demo\n\n## Why\nBecause reasons that are sufficiently long.\n\n## What Changes\n- **spec-x:** Add something`;
await fs.mkdir(path.join(changesDir, 'demo'), { recursive: true });
await fs.writeFile(path.join(changesDir, 'demo', 'proposal.md'), content, 'utf-8');
});
afterEach(async () => {
await fs.rm(testDir, { recursive: true, force: true });
});
it('prints list hint and exits non-zero when no arg and non-interactive', () => {
const originalCwd = process.cwd();
const originalEnv = { ...process.env };
try {
process.chdir(testDir);
process.env.OPEN_SPEC_INTERACTIVE = '0';
let err: any;
try {
execSync(`node ${bin} change validate`, { encoding: 'utf-8' });
} catch (e) { err = e; }
expect(err).toBeDefined();
expect(err.status).not.toBe(0);
expect(err.stderr.toString()).toContain('Available IDs:');
expect(err.stderr.toString()).toContain('openspec change list');
} finally {
process.chdir(originalCwd);
process.env = originalEnv;
}
});
});
+123
View File
@@ -0,0 +1,123 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { promises as fs } from 'fs';
import path from 'path';
import { execSync } from 'child_process';
describe('top-level show command', () => {
const projectRoot = process.cwd();
const testDir = path.join(projectRoot, 'test-show-command-tmp');
const changesDir = path.join(testDir, 'openspec', 'changes');
const specsDir = path.join(testDir, 'openspec', 'specs');
const openspecBin = path.join(projectRoot, 'bin', 'openspec.js');
beforeEach(async () => {
await fs.mkdir(changesDir, { recursive: true });
await fs.mkdir(specsDir, { recursive: true });
const changeContent = `# Change: Demo\n\n## Why\nBecause reasons.\n\n## What Changes\n- **auth:** Add requirement\n`;
await fs.mkdir(path.join(changesDir, 'demo'), { recursive: true });
await fs.writeFile(path.join(changesDir, 'demo', 'proposal.md'), changeContent, 'utf-8');
const specContent = `## Purpose\nAuth spec.\n\n## Requirements\n\n### Requirement: User Authentication\nText\n`;
await fs.mkdir(path.join(specsDir, 'auth'), { recursive: true });
await fs.writeFile(path.join(specsDir, 'auth', 'spec.md'), specContent, 'utf-8');
});
afterEach(async () => {
await fs.rm(testDir, { recursive: true, force: true });
});
it('prints hint and non-zero exit when no args and non-interactive', () => {
const originalCwd = process.cwd();
const originalEnv = { ...process.env };
try {
process.chdir(testDir);
process.env.OPEN_SPEC_INTERACTIVE = '0';
let err: any;
try {
execSync(`node ${openspecBin} show`, { encoding: 'utf-8' });
} catch (e) { err = e; }
expect(err).toBeDefined();
expect(err.status).not.toBe(0);
const stderr = err.stderr.toString();
expect(stderr).toContain('Nothing to show.');
expect(stderr).toContain('openspec show <item>');
expect(stderr).toContain('openspec change show');
expect(stderr).toContain('openspec spec show');
} finally {
process.chdir(originalCwd);
process.env = originalEnv;
}
});
it('auto-detects change id and supports --json', () => {
const originalCwd = process.cwd();
try {
process.chdir(testDir);
const output = execSync(`node ${openspecBin} show demo --json`, { encoding: 'utf-8' });
const json = JSON.parse(output);
expect(json.id).toBe('demo');
expect(Array.isArray(json.deltas)).toBe(true);
} finally {
process.chdir(originalCwd);
}
});
it('auto-detects spec id and supports spec-only flags', () => {
const originalCwd = process.cwd();
try {
process.chdir(testDir);
const output = execSync(`node ${openspecBin} show auth --json --requirements`, { encoding: 'utf-8' });
const json = JSON.parse(output);
expect(json.id).toBe('auth');
expect(Array.isArray(json.requirements)).toBe(true);
} finally {
process.chdir(originalCwd);
}
});
it('handles ambiguity and suggests --type', async () => {
// create matching spec and change named 'foo'
await fs.mkdir(path.join(changesDir, 'foo'), { recursive: true });
await fs.writeFile(path.join(changesDir, 'foo', 'proposal.md'), '# Change: Foo\n\n## Why\n\n## What Changes\n', 'utf-8');
await fs.mkdir(path.join(specsDir, 'foo'), { recursive: true });
await fs.writeFile(path.join(specsDir, 'foo', 'spec.md'), '## Purpose\n\n## Requirements\n\n### Requirement: R\nX', 'utf-8');
const originalCwd = process.cwd();
try {
process.chdir(testDir);
let err: any;
try {
execSync(`node ${openspecBin} show foo`, { encoding: 'utf-8' });
} catch (e) { err = e; }
expect(err).toBeDefined();
expect(err.status).not.toBe(0);
const stderr = err.stderr.toString();
expect(stderr).toContain('Ambiguous item');
expect(stderr).toContain('--type change|spec');
} finally {
process.chdir(originalCwd);
}
});
it('prints nearest matches when not found', () => {
const originalCwd = process.cwd();
try {
process.chdir(testDir);
let err: any;
try {
execSync(`node ${openspecBin} show unknown-item`, { encoding: 'utf-8' });
} catch (e) { err = e; }
expect(err).toBeDefined();
expect(err.status).not.toBe(0);
const stderr = err.stderr.toString();
expect(stderr).toContain("Unknown item 'unknown-item'");
expect(stderr).toContain('Did you mean:');
} finally {
process.chdir(originalCwd);
}
});
});
@@ -0,0 +1,44 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { promises as fs } from 'fs';
import path from 'path';
import { execSync } from 'child_process';
describe('spec show (interactive behavior)', () => {
const projectRoot = process.cwd();
const testDir = path.join(projectRoot, 'test-spec-show-tmp');
const specsDir = path.join(testDir, 'openspec', 'specs');
const bin = path.join(projectRoot, 'bin', 'openspec.js');
beforeEach(async () => {
await fs.mkdir(specsDir, { recursive: true });
const content = `## Purpose\nX\n\n## Requirements\n\n### Requirement: R\nText`;
await fs.mkdir(path.join(specsDir, 's1'), { recursive: true });
await fs.writeFile(path.join(specsDir, 's1', 'spec.md'), content, 'utf-8');
});
afterEach(async () => {
await fs.rm(testDir, { recursive: true, force: true });
});
it('errors when no arg and non-interactive', () => {
const originalCwd = process.cwd();
const originalEnv = { ...process.env };
try {
process.chdir(testDir);
process.env.OPEN_SPEC_INTERACTIVE = '0';
let err: any;
try {
execSync(`node ${bin} spec show`, { encoding: 'utf-8' });
} catch (e) { err = e; }
expect(err).toBeDefined();
expect(err.status).not.toBe(0);
expect(err.stderr.toString()).toContain('Missing required argument <spec-id>');
} finally {
process.chdir(originalCwd);
process.env = originalEnv;
}
});
});
@@ -0,0 +1,44 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { promises as fs } from 'fs';
import path from 'path';
import { execSync } from 'child_process';
describe('spec validate (interactive behavior)', () => {
const projectRoot = process.cwd();
const testDir = path.join(projectRoot, 'test-spec-validate-tmp');
const specsDir = path.join(testDir, 'openspec', 'specs');
const bin = path.join(projectRoot, 'bin', 'openspec.js');
beforeEach(async () => {
await fs.mkdir(specsDir, { recursive: true });
const content = `## Purpose\nValid spec for interactive test.\n\n## Requirements\n\n### Requirement: X\nText`;
await fs.mkdir(path.join(specsDir, 's1'), { recursive: true });
await fs.writeFile(path.join(specsDir, 's1', 'spec.md'), content, 'utf-8');
});
afterEach(async () => {
await fs.rm(testDir, { recursive: true, force: true });
});
it('errors when no arg and non-interactive', () => {
const originalCwd = process.cwd();
const originalEnv = { ...process.env };
try {
process.chdir(testDir);
process.env.OPEN_SPEC_INTERACTIVE = '0';
let err: any;
try {
execSync(`node ${bin} spec validate`, { encoding: 'utf-8' });
} catch (e) { err = e; }
expect(err).toBeDefined();
expect(err.status).not.toBe(0);
expect(err.stderr.toString()).toContain('Missing required argument <spec-id>');
} finally {
process.chdir(originalCwd);
process.env = originalEnv;
}
});
});
+1 -5
View File
@@ -1,4 +1,4 @@
import { describe, it, expect, beforeEach, afterEach, beforeAll } from 'vitest';
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { promises as fs } from 'fs';
import path from 'path';
import { execSync } from 'child_process';
@@ -9,10 +9,6 @@ describe('spec command', () => {
const specsDir = path.join(testDir, 'openspec', 'specs');
const openspecBin = path.join(projectRoot, 'bin', 'openspec.js');
beforeAll(() => {
// Ensure CLI is built so bin/openspec.js loads latest logic from dist/
execSync('pnpm -s build', { stdio: 'pipe' });
});
beforeEach(async () => {
await fs.mkdir(specsDir, { recursive: true });
@@ -0,0 +1,49 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { promises as fs } from 'fs';
import path from 'path';
import { execSync } from 'child_process';
describe('validate command enriched human output', () => {
const projectRoot = process.cwd();
const testDir = path.join(projectRoot, 'test-validate-enriched-tmp');
const changesDir = path.join(testDir, 'openspec', 'changes');
const bin = path.join(projectRoot, 'bin', 'openspec.js');
beforeEach(async () => {
await fs.mkdir(changesDir, { recursive: true });
});
afterEach(async () => {
await fs.rm(testDir, { recursive: true, force: true });
});
it('prints Next steps footer and guidance on invalid change', () => {
const changeContent = `# Test Change\n\n## Why\nThis is a sufficiently long explanation to pass the why length requirement for validation purposes.\n\n## What Changes\nThere are changes proposed, but no delta specs provided yet.`;
const changeId = 'c-next-steps';
const changePath = path.join(changesDir, changeId);
execSync(`mkdir -p ${changePath}`);
execSync(`bash -lc "cat > ${path.join(changePath, 'proposal.md')} <<'EOF'\n${changeContent}\nEOF"`);
const originalCwd = process.cwd();
try {
process.chdir(testDir);
let code = 0;
let stderr = '';
try {
execSync(`node ${bin} change validate ${changeId}`, { encoding: 'utf-8', stdio: 'pipe' });
} catch (e: any) {
code = e?.status ?? 1;
stderr = e?.stderr?.toString?.() ?? '';
}
expect(code).not.toBe(0);
expect(stderr).toContain('has issues');
expect(stderr).toContain('Next steps:');
expect(stderr).toContain('openspec change show');
} finally {
process.chdir(originalCwd);
}
});
});

Some files were not shown because too many files have changed in this diff Show More