Compare commits

...
Author SHA1 Message Date
Tabish Bidiwale ccaa5ad0b0 feat: add openspec view dashboard command 2025-09-12 21:11:42 +10:00
Tabish Bidiwale be395d9d74 docs(readme): shrink header logo to 64px 2025-09-12 16:34:17 +10:00
Tabish Bidiwale a09b87e391 docs(readme): add adaptive logo and center badges 2025-09-12 16:32:33 +10:00
Tabish Bidiwale 44c06cc40e Merge pull request #58 from Fission-AI/docs/align-agent-instructions
docs(openspec): align agent instructions and templates
2025-09-10 10:38:29 +10:00
Tabish Bidiwale b33f435812 docs(template): sync readme-template to exactly match openspec/README.md 2025-09-09 22:15:39 +10:00
Tabish Bidiwale b62b57caf3 fix(templates): escape backticks and move examples inside template literals 2025-09-09 22:02:34 +10:00
Tabish Bidiwale 758d91613d docs(openspec): prefer CLI examples for listing/showing; keep rg for full-text search 2025-09-09 21:50:05 +10:00
Tabish Bidiwale 47180e5120 docs(openspec): add TL;DR, search guidance, design skeleton, renamed example, approval gate, and examples 2025-09-09 21:36:17 +10:00
Tabish Bidiwale 8dde1d55fc docs(openspec): align agent instructions and templates 2025-09-09 21:26:56 +10:00
Tabish Bidiwale 3cef6f0925 Merge pull request #57 from Fission-AI/remove-diff-command
Remove diff command in favor of show command
2025-09-09 14:15:34 +10:00
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
44 changed files with 2819 additions and 1111 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": []
}
+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
+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.
+242 -79
View File
@@ -1,119 +1,282 @@
<p align="center">
<a href="https://github.com/Fission-AI/OpenSpec">
<picture>
<source srcset="assets/openspec_pixel_dark.svg" media="(prefers-color-scheme: dark)">
<source srcset="assets/openspec_pixel_light.svg" media="(prefers-color-scheme: light)">
<img src="assets/openspec_pixel_light.svg" alt="OpenSpec logo" height="64">
</picture>
</a>
</p>
<p align="center">Spec-driven development for AI coding assistants.</p>
<p align="center">
<a href="https://github.com/Fission-AI/OpenSpec/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/Fission-AI/OpenSpec/actions/workflows/ci.yml/badge.svg" /></a>
<a href="https://www.npmjs.com/package/@fission-ai/openspec"><img alt="npm version" src="https://img.shields.io/npm/v/@fission-ai/openspec?style=flat-square" /></a>
<a href="https://nodejs.org/"><img alt="node version" src="https://img.shields.io/node/v/@fission-ai/openspec?style=flat-square" /></a>
<a href="./LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square" /></a>
<a href="https://conventionalcommits.org"><img alt="Conventional Commits" src="https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg?style=flat-square" /></a>
</p>
# OpenSpec
A specification-driven development system for maintaining living documentation alongside your code.
**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
+89
View File
@@ -0,0 +1,89 @@
<svg width="640" height="80" viewBox="0 0 640 80" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M32 0H16V16H32V0Z" fill="white"/>
<path d="M48 0H32V16H48V0Z" fill="white"/>
<path d="M16 16H0V32H16V16Z" fill="white"/>
<path d="M64 16H48V32H64V16Z" fill="white"/>
<path d="M16 32H0V48H16V32Z" fill="white"/>
<path d="M64 32H48V48H64V32Z" fill="white"/>
<path d="M16 48H0V64H16V48Z" fill="white"/>
<path d="M64 48H48V64H64V48Z" fill="white"/>
<path d="M32 64H16V80H32V64Z" fill="white"/>
<path d="M48 64H32V80H48V64Z" fill="white"/>
<path d="M96 0H80V16H96V0Z" fill="white"/>
<path d="M112 0H96V16H112V0Z" fill="white"/>
<path d="M128 0H112V16H128V0Z" fill="white"/>
<path d="M96 16H80V32H96V16Z" fill="white"/>
<path d="M144 16H128V32H144V16Z" fill="white"/>
<path d="M96 32H80V48H96V32Z" fill="white"/>
<path d="M112 32H96V48H112V32Z" fill="white"/>
<path d="M128 32H112V48H128V32Z" fill="white"/>
<path d="M144 32H128V48H144V32Z" fill="white"/>
<path d="M96 48H80V64H96V48Z" fill="white"/>
<path d="M96 64H80V80H96V64Z" fill="white"/>
<path d="M176 0H160V16H176V0Z" fill="white"/>
<path d="M192 0H176V16H192V0Z" fill="white"/>
<path d="M208 0H192V16H208V0Z" fill="white"/>
<path d="M224 0H208V16H224V0Z" fill="white"/>
<path d="M176 16H160V32H176V16Z" fill="white"/>
<path d="M176 32H160V48H176V32Z" fill="white"/>
<path d="M192 32H176V48H192V32Z" fill="white"/>
<path d="M208 32H192V48H208V32Z" fill="white"/>
<path d="M176 48H160V64H176V48Z" fill="white"/>
<path d="M176 64H160V80H176V64Z" fill="white"/>
<path d="M192 64H176V80H192V64Z" fill="white"/>
<path d="M208 64H192V80H208V64Z" fill="white"/>
<path d="M224 64H208V80H224V64Z" fill="white"/>
<path d="M256 0H240V16H256V0Z" fill="white"/>
<path d="M304 0H288V16H304V0Z" fill="white"/>
<path d="M256 16H240V32H256V16Z" fill="white"/>
<path d="M272 16H256V32H272V16Z" fill="white"/>
<path d="M304 16H288V32H304V16Z" fill="white"/>
<path d="M256 32H240V48H256V32Z" fill="white"/>
<path d="M288 32H272V48H288V32Z" fill="white"/>
<path d="M304 32H288V48H304V32Z" fill="white"/>
<path d="M256 48H240V64H256V48Z" fill="white"/>
<path d="M304 48H288V64H304V48Z" fill="white"/>
<path d="M256 64H240V80H256V64Z" fill="white"/>
<path d="M304 64H288V80H304V64Z" fill="white"/>
<path d="M352 0H336V16H352V0Z" fill="white"/>
<path d="M368 0H352V16H368V0Z" fill="white"/>
<path d="M384 0H368V16H384V0Z" fill="white"/>
<path d="M336 16H320V32H336V16Z" fill="white"/>
<path d="M352 32H336V48H352V32Z" fill="white"/>
<path d="M368 32H352V48H368V32Z" fill="white"/>
<path d="M384 48H368V64H384V48Z" fill="white"/>
<path d="M336 64H320V80H336V64Z" fill="white"/>
<path d="M352 64H336V80H352V64Z" fill="white"/>
<path d="M368 64H352V80H368V64Z" fill="white"/>
<path d="M416 0H400V16H416V0Z" fill="white"/>
<path d="M432 0H416V16H432V0Z" fill="white"/>
<path d="M448 0H432V16H448V0Z" fill="white"/>
<path d="M416 16H400V32H416V16Z" fill="white"/>
<path d="M464 16H448V32H464V16Z" fill="white"/>
<path d="M416 32H400V48H416V32Z" fill="white"/>
<path d="M432 32H416V48H432V32Z" fill="white"/>
<path d="M448 32H432V48H448V32Z" fill="white"/>
<path d="M464 32H448V48H464V32Z" fill="white"/>
<path d="M416 48H400V64H416V48Z" fill="white"/>
<path d="M416 64H400V80H416V64Z" fill="white"/>
<path d="M496 0H480V16H496V0Z" fill="white"/>
<path d="M512 0H496V16H512V0Z" fill="white"/>
<path d="M528 0H512V16H528V0Z" fill="white"/>
<path d="M544 0H528V16H544V0Z" fill="white"/>
<path d="M496 16H480V32H496V16Z" fill="white"/>
<path d="M496 32H480V48H496V32Z" fill="white"/>
<path d="M512 32H496V48H512V32Z" fill="white"/>
<path d="M528 32H512V48H528V32Z" fill="white"/>
<path d="M496 48H480V64H496V48Z" fill="white"/>
<path d="M496 64H480V80H496V64Z" fill="white"/>
<path d="M512 64H496V80H512V64Z" fill="white"/>
<path d="M528 64H512V80H528V64Z" fill="white"/>
<path d="M544 64H528V80H544V64Z" fill="white"/>
<path d="M592 0H576V16H592V0Z" fill="white"/>
<path d="M608 0H592V16H608V0Z" fill="white"/>
<path d="M576 16H560V32H576V16Z" fill="white"/>
<path d="M576 32H560V48H576V32Z" fill="white"/>
<path d="M576 48H560V64H576V48Z" fill="white"/>
<path d="M592 64H576V80H592V64Z" fill="white"/>
<path d="M608 64H592V80H608V64Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

+89
View File
@@ -0,0 +1,89 @@
<svg xmlns="http://www.w3.org/2000/svg" width="640" height="80" viewBox="0 0 640 80">
<rect x="16" y="0" width="16" height="16" fill="black" />
<rect x="32" y="0" width="16" height="16" fill="black" />
<rect x="0" y="16" width="16" height="16" fill="black" />
<rect x="48" y="16" width="16" height="16" fill="black" />
<rect x="0" y="32" width="16" height="16" fill="black" />
<rect x="48" y="32" width="16" height="16" fill="black" />
<rect x="0" y="48" width="16" height="16" fill="black" />
<rect x="48" y="48" width="16" height="16" fill="black" />
<rect x="16" y="64" width="16" height="16" fill="black" />
<rect x="32" y="64" width="16" height="16" fill="black" />
<rect x="80" y="0" width="16" height="16" fill="black" />
<rect x="96" y="0" width="16" height="16" fill="black" />
<rect x="112" y="0" width="16" height="16" fill="black" />
<rect x="80" y="16" width="16" height="16" fill="black" />
<rect x="128" y="16" width="16" height="16" fill="black" />
<rect x="80" y="32" width="16" height="16" fill="black" />
<rect x="96" y="32" width="16" height="16" fill="black" />
<rect x="112" y="32" width="16" height="16" fill="black" />
<rect x="128" y="32" width="16" height="16" fill="black" />
<rect x="80" y="48" width="16" height="16" fill="black" />
<rect x="80" y="64" width="16" height="16" fill="black" />
<rect x="160" y="0" width="16" height="16" fill="black" />
<rect x="176" y="0" width="16" height="16" fill="black" />
<rect x="192" y="0" width="16" height="16" fill="black" />
<rect x="208" y="0" width="16" height="16" fill="black" />
<rect x="160" y="16" width="16" height="16" fill="black" />
<rect x="160" y="32" width="16" height="16" fill="black" />
<rect x="176" y="32" width="16" height="16" fill="black" />
<rect x="192" y="32" width="16" height="16" fill="black" />
<rect x="160" y="48" width="16" height="16" fill="black" />
<rect x="160" y="64" width="16" height="16" fill="black" />
<rect x="176" y="64" width="16" height="16" fill="black" />
<rect x="192" y="64" width="16" height="16" fill="black" />
<rect x="208" y="64" width="16" height="16" fill="black" />
<rect x="240" y="0" width="16" height="16" fill="black" />
<rect x="288" y="0" width="16" height="16" fill="black" />
<rect x="240" y="16" width="16" height="16" fill="black" />
<rect x="256" y="16" width="16" height="16" fill="black" />
<rect x="288" y="16" width="16" height="16" fill="black" />
<rect x="240" y="32" width="16" height="16" fill="black" />
<rect x="272" y="32" width="16" height="16" fill="black" />
<rect x="288" y="32" width="16" height="16" fill="black" />
<rect x="240" y="48" width="16" height="16" fill="black" />
<rect x="288" y="48" width="16" height="16" fill="black" />
<rect x="240" y="64" width="16" height="16" fill="black" />
<rect x="288" y="64" width="16" height="16" fill="black" />
<rect x="336" y="0" width="16" height="16" fill="black" />
<rect x="352" y="0" width="16" height="16" fill="black" />
<rect x="368" y="0" width="16" height="16" fill="black" />
<rect x="320" y="16" width="16" height="16" fill="black" />
<rect x="336" y="32" width="16" height="16" fill="black" />
<rect x="352" y="32" width="16" height="16" fill="black" />
<rect x="368" y="48" width="16" height="16" fill="black" />
<rect x="320" y="64" width="16" height="16" fill="black" />
<rect x="336" y="64" width="16" height="16" fill="black" />
<rect x="352" y="64" width="16" height="16" fill="black" />
<rect x="400" y="0" width="16" height="16" fill="black" />
<rect x="416" y="0" width="16" height="16" fill="black" />
<rect x="432" y="0" width="16" height="16" fill="black" />
<rect x="400" y="16" width="16" height="16" fill="black" />
<rect x="448" y="16" width="16" height="16" fill="black" />
<rect x="400" y="32" width="16" height="16" fill="black" />
<rect x="416" y="32" width="16" height="16" fill="black" />
<rect x="432" y="32" width="16" height="16" fill="black" />
<rect x="448" y="32" width="16" height="16" fill="black" />
<rect x="400" y="48" width="16" height="16" fill="black" />
<rect x="400" y="64" width="16" height="16" fill="black" />
<rect x="480" y="0" width="16" height="16" fill="black" />
<rect x="496" y="0" width="16" height="16" fill="black" />
<rect x="512" y="0" width="16" height="16" fill="black" />
<rect x="528" y="0" width="16" height="16" fill="black" />
<rect x="480" y="16" width="16" height="16" fill="black" />
<rect x="480" y="32" width="16" height="16" fill="black" />
<rect x="496" y="32" width="16" height="16" fill="black" />
<rect x="512" y="32" width="16" height="16" fill="black" />
<rect x="480" y="48" width="16" height="16" fill="black" />
<rect x="480" y="64" width="16" height="16" fill="black" />
<rect x="496" y="64" width="16" height="16" fill="black" />
<rect x="512" y="64" width="16" height="16" fill="black" />
<rect x="528" y="64" width="16" height="16" fill="black" />
<rect x="576" y="0" width="16" height="16" fill="black" />
<rect x="592" y="0" width="16" height="16" fill="black" />
<rect x="560" y="16" width="16" height="16" fill="black" />
<rect x="560" y="32" width="16" height="16" fill="black" />
<rect x="560" y="48" width="16" height="16" fill="black" />
<rect x="576" y="64" width="16" height="16" fill="black" />
<rect x="592" y="64" width="16" height="16" fill="black" />
</svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

+139 -5
View File
@@ -2,6 +2,16 @@
Instructions for AI coding assistants using OpenSpec for spec-driven development.
## TL;DR Quick Checklist
- Search existing work: `openspec spec list --long`, `openspec list` (use `rg` only for full-text search)
- Decide scope: new capability vs modify existing capability
- Pick a unique `change-id`: kebab-case, verb-led (`add-`, `update-`, `remove-`, `refactor-`)
- Scaffold: `proposal.md`, `tasks.md`, `design.md` (only if needed), and delta specs per affected capability
- Write deltas: use `## ADDED|MODIFIED|REMOVED|RENAMED Requirements`; include at least one `#### Scenario:` per requirement
- Validate: `openspec validate [change-id] --strict` and fix issues
- Request approval: Do not start implementation until proposal is approved
## Three-Stage Workflow
### Stage 1: Creating Changes
@@ -12,6 +22,17 @@ Create proposal when you need to:
- Optimize performance (changes behavior)
- Update security patterns
Triggers (examples):
- "Help me create a change proposal"
- "Help me plan a change"
- "Help me create a proposal"
- "I want to create a spec proposal"
- "I want to create a spec"
Loose matching guidance:
- Contains one of: `proposal`, `change`, `spec`
- With one of: `create`, `plan`, `make`, `start`, `help`
Skip proposal for:
- Bug fixes (restore intended behavior)
- Typos, formatting, comments
@@ -25,6 +46,8 @@ Skip proposal for:
3. **Read tasks.md** - Get implementation checklist
4. **Implement tasks sequentially** - Complete in order
5. **Mark complete immediately** - Update `- [x]` after each task
6. **Validate strictly** - Run `openspec validate [change] --strict` and address issues
7. **Approval gate** - Do not start implementation until the proposal is reviewed and approved
### Stage 3: Archiving Changes
After deployment, create separate PR to:
@@ -45,6 +68,15 @@ After deployment, create separate PR to:
- Always check if capability already exists
- Prefer modifying existing specs over creating duplicates
- Use `openspec show [spec]` to review current state
- If request is ambiguous, ask 1–2 clarifying questions before scaffolding
### Search Guidance
- Enumerate specs: `openspec spec list --long` (or `--json` for scripts)
- Enumerate changes: `openspec list` (or `openspec change list --json` - deprecated but available)
- Show details:
- Spec: `openspec show <spec-id> --type spec` (use `--json` for filters)
- Change: `openspec show <change-id> --json --deltas-only`
- Full-text search (use ripgrep): `rg -n "Requirement:|Scenario:" openspec/specs`
## Quick Start
@@ -55,7 +87,6 @@ After deployment, create separate PR to:
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
@@ -93,7 +124,7 @@ openspec/
│ ├── [change-name]/
│ │ ├── proposal.md # Why, what, impact
│ │ ├── tasks.md # Implementation checklist
│ │ ├── design.md # Technical decisions (optional)
│ │ ├── design.md # Technical decisions (optional; see criteria)
│ │ └── specs/ # Delta changes
│ │ └── [capability]/
│ │ └── spec.md # ADDED/MODIFIED/REMOVED
@@ -116,7 +147,7 @@ New request?
### Proposal Structure
1. **Create directory:** `changes/[descriptive-name]/`
1. **Create directory:** `changes/[change-id]/` (kebab-case, verb-led, unique)
2. **Write proposal.md:**
```markdown
@@ -151,6 +182,7 @@ The system SHALL provide...
**Reason**: [Why removing]
**Migration**: [How to handle]
```
If multiple capabilities are affected, create multiple delta files under `changes/[change-id]/specs/<capability>/spec.md`—one per capability.
4. **Create tasks.md:**
```markdown
@@ -161,6 +193,36 @@ The system SHALL provide...
- [ ] 1.4 Write tests
```
5. **Create design.md when needed:**
Create `design.md` if any of the following apply; otherwise omit it:
- Cross-cutting change (multiple services/modules) or a new architectural pattern
- New external dependency or significant data model changes
- Security, performance, or migration complexity
- Ambiguity that benefits from technical decisions before coding
Minimal `design.md` skeleton:
```markdown
## Context
[Background, constraints, stakeholders]
## Goals / Non-Goals
- Goals: [...]
- Non-Goals: [...]
## Decisions
- Decision: [What and why]
- Alternatives considered: [Options + rationale]
## Risks / Trade-offs
- [Risk] → Mitigation
## Migration Plan
[Steps, rollback]
## Open Questions
- [...]
```
## Spec File Format
### Critical: Scenario Formatting
@@ -181,6 +243,9 @@ The system SHALL provide...
Every requirement MUST have at least one scenario.
### Requirement Wording
- Use SHALL/MUST for normative requirements (avoid should/may unless intentionally non-normative)
### Delta Operations
- `## ADDED Requirements` - New capabilities
@@ -190,6 +255,13 @@ Every requirement MUST have at least one scenario.
Headers matched with `trim(header)` - whitespace ignored.
Example for RENAMED:
```markdown
## RENAMED Requirements
- FROM: `### Requirement: Login`
- TO: `### Requirement: User Authentication`
```
## Troubleshooting
### Common Errors
@@ -219,6 +291,64 @@ openspec show [change] --json | jq '.deltas'
openspec show [spec] --json -r 1
```
## Happy Path Script
```bash
# 1) Explore current state
openspec spec list --long
openspec list
# Optional full-text search:
# rg -n "Requirement:|Scenario:" openspec/specs
# rg -n "^#|Requirement:" openspec/changes
# 2) Choose change id and scaffold
CHANGE=add-two-factor-auth
mkdir -p openspec/changes/$CHANGE/{specs/auth}
printf "## Why\n...\n\n## What Changes\n- ...\n\n## Impact\n- ...\n" > openspec/changes/$CHANGE/proposal.md
printf "## 1. Implementation\n- [ ] 1.1 ...\n" > openspec/changes/$CHANGE/tasks.md
# 3) Add deltas (example)
cat > openspec/changes/$CHANGE/specs/auth/spec.md << 'EOF'
## ADDED Requirements
### Requirement: Two-Factor Authentication
Users MUST provide a second factor during login.
#### Scenario: OTP required
- **WHEN** valid credentials are provided
- **THEN** an OTP challenge is required
EOF
# 4) Validate
openspec validate $CHANGE --strict
```
## Multi-Capability Example
```
openspec/changes/add-2fa-notify/
├── proposal.md
├── tasks.md
└── specs/
├── auth/
│ └── spec.md # ADDED: Two-Factor Authentication
└── notifications/
└── spec.md # ADDED: OTP email notification
```
auth/spec.md
```markdown
## ADDED Requirements
### Requirement: Two-Factor Authentication
...
```
notifications/spec.md
```markdown
## ADDED Requirements
### Requirement: OTP Email Notification
...
```
## Best Practices
### Simplicity First
@@ -244,6 +374,11 @@ Only add complexity with:
- 10-minute understandability rule
- Split if description needs "AND"
### Change ID Naming
- Use kebab-case, short and descriptive: `add-two-factor-auth`
- Prefer verb-led prefixes: `add-`, `update-`, `remove-`, `refactor-`
- Ensure uniqueness; if taken, append `-2`, `-3`, etc.
## Tool Selection Guide
| Task | Tool | Why |
@@ -290,9 +425,8 @@ Only add complexity with:
```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.
Remember: Specs are truth. Changes are proposals. Keep them in sync.
@@ -1,57 +0,0 @@
# 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,38 @@
# Change: Add View Dashboard Command
## Why
Users need a quick, at-a-glance overview of their OpenSpec project status without running multiple commands. Currently, users must run `openspec list --changes` and `openspec list --specs` separately to understand the project state. A unified dashboard view would improve developer experience and provide immediate insight into project progress.
## What Changes
### Added `openspec view` Command
The new command provides an interactive dashboard displaying:
- Summary metrics (total specs, requirements, changes, task progress)
- Active changes with visual progress bars
- Completed changes
- Specifications with requirement counts
### Specifications Affected
- **cli-view** (NEW): Complete specification for the view dashboard command
## Implementation Details
### File Structure
- Created `/src/core/view.ts` implementing the `ViewCommand` class
- Registered command in `/src/cli/index.ts`
- Reuses existing utilities from `task-progress.ts` and `MarkdownParser`
### Visual Design
- Uses Unicode box drawing characters for borders
- Color coding: cyan for specs, yellow for active, green for completed
- Progress bars using filled (█) and empty (░) blocks
- Clean alignment with proper padding
### Technical Approach
- Async data fetching from changes and specs directories
- Parallel processing of specs and changes
- Error handling for missing or invalid data
- Maintains consistency with existing list command output
@@ -0,0 +1,109 @@
# CLI View Command - Changes
## ADDED Requirements
### Requirement: Dashboard Display
The system SHALL provide a `view` command that displays a dashboard overview of specs and changes.
#### Scenario: Basic dashboard display
- **WHEN** user runs `openspec view`
- **THEN** system displays a formatted dashboard with sections for summary, active changes, completed changes, and specifications
#### Scenario: No OpenSpec directory
- **WHEN** user runs `openspec view` in a directory without OpenSpec
- **THEN** system displays error message "✗ No openspec directory found"
### Requirement: Summary Section
The dashboard SHALL display a summary section with key project metrics.
#### Scenario: Complete summary display
- **WHEN** dashboard is rendered with specs and changes
- **THEN** system shows total number of specifications and requirements
- **AND** shows number of active changes in progress
- **AND** shows number of completed changes
- **AND** shows overall task progress percentage
#### Scenario: Empty project summary
- **WHEN** no specs or changes exist
- **THEN** summary shows zero counts for all metrics
### Requirement: Active Changes Display
The dashboard SHALL show active changes with visual progress indicators.
#### Scenario: Active changes with progress bars
- **WHEN** there are in-progress changes with tasks
- **THEN** system displays each change with change name left-aligned
- **AND** visual progress bar using Unicode characters
- **AND** percentage completion on the right
#### Scenario: No active changes
- **WHEN** all changes are completed or no changes exist
- **THEN** active changes section is omitted from display
### Requirement: Completed Changes Display
The dashboard SHALL list completed changes in a separate section.
#### Scenario: Completed changes listing
- **WHEN** there are completed changes (all tasks done)
- **THEN** system shows them with checkmark indicators in a dedicated section
#### Scenario: Mixed completion states
- **WHEN** some changes are complete and others active
- **THEN** system separates them into appropriate sections
### Requirement: Specifications Display
The dashboard SHALL display specifications sorted by requirement count.
#### Scenario: Specs listing with counts
- **WHEN** specifications exist in the project
- **THEN** system shows specs sorted by requirement count (descending) with count labels
#### Scenario: Specs with parsing errors
- **WHEN** a spec file cannot be parsed
- **THEN** system includes it with 0 requirement count
### Requirement: Visual Formatting
The dashboard SHALL use consistent visual formatting with colors and symbols.
#### Scenario: Color coding
- **WHEN** dashboard elements are displayed
- **THEN** system uses cyan for specification items
- **AND** yellow for active changes
- **AND** green for completed items
- **AND** dim gray for supplementary text
#### Scenario: Progress bar rendering
- **WHEN** displaying progress bars
- **THEN** system uses filled blocks (█) for completed portions and light blocks (░) for remaining
### Requirement: Error Handling
The view command SHALL handle errors gracefully.
#### Scenario: File system errors
- **WHEN** file system operations fail
- **THEN** system continues with available data and omits inaccessible items
#### Scenario: Invalid data structures
- **WHEN** specs or changes have invalid format
- **THEN** system skips invalid items and continues rendering
@@ -0,0 +1,47 @@
# Implementation Tasks
## Design Phase
- [x] Research existing list command implementation
- [x] Design dashboard layout and information architecture
- [x] Choose appropriate command verb (`view`)
- [x] Define visual elements (progress bars, colors, layout)
## Core Implementation
- [x] Create ViewCommand class in `/src/core/view.ts`
- [x] Implement getChangesData method for fetching change information
- [x] Implement getSpecsData method for fetching spec information
- [x] Implement displaySummary method for summary metrics
- [x] Add progress bar visualization with Unicode characters
- [x] Implement color coding using chalk
## Integration
- [x] Import ViewCommand in CLI index
- [x] Register `openspec view` command with commander
- [x] Add proper error handling and ora spinner integration
- [x] Ensure command appears in help documentation
## Data Processing
- [x] Reuse TaskProgress utilities for change progress
- [x] Integrate MarkdownParser for spec requirement counting
- [x] Handle async operations for file system access
- [x] Sort specifications by requirement count
## Testing and Validation
- [x] Build project successfully with new command
- [x] Test command with sample data
- [x] Verify correct requirement counts match list --specs
- [x] Test progress bar display for various completion states
- [x] Run existing test suite to ensure no regressions
- [x] Verify TypeScript compilation with no errors
## Documentation
- [x] Add command description in CLI help
- [x] Create change proposal documentation
- [x] Update README with view command example (if needed)
- [x] Add view command to user documentation (if exists)
## Polish
- [x] Ensure consistent formatting and alignment
- [x] Add helpful footer text referencing list commands
- [x] Optimize for terminal width considerations
- [x] Review and refine color choices for accessibility
@@ -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
-123
View File
@@ -1,123 +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 show a requirement-level comparison displaying only changed requirements.
#### Scenario: Side-by-side comparison of changes
- **WHEN** running `openspec diff <change>`
- **THEN** display only requirements that have changed
- **AND** show them in a side-by-side format that:
- Clearly shows the current version on the left
- Shows the future version on the right
- Indicates new requirements (not in current)
- Indicates removed requirements (not in future)
- Aligns modified requirements for easy comparison
### Requirement: 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"
### Requirement: Validation
The command SHALL validate that changes can be applied successfully.
#### Scenario: Invalid delta references
- **WHEN** delta references non-existent requirement
- **THEN** show error message with specific requirement
- **AND** continue showing other valid changes
- **AND** clearly mark failed changes in the output
### Requirement: Diff Command Enhancement
The diff command SHALL validate change structure before displaying differences.
#### Scenario: Validate before diff
- **WHEN** executing `openspec diff change-name`
- **THEN** validate change structure
- **AND** show validation warnings if present
- **AND** continue with diff display
## 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):
```
+112
View File
@@ -0,0 +1,112 @@
# cli-view Specification
## Purpose
The `openspec view` command provides a comprehensive dashboard view of the OpenSpec project state, displaying specifications, changes, and progress metrics in a unified, visually appealing format to help developers quickly understand project status.
## Requirements
### Requirement: Dashboard Display
The system SHALL provide a `view` command that displays a dashboard overview of specs and changes.
#### Scenario: Basic dashboard display
- **WHEN** user runs `openspec view`
- **THEN** system displays a formatted dashboard with sections for summary, active changes, completed changes, and specifications
#### Scenario: No OpenSpec directory
- **WHEN** user runs `openspec view` in a directory without OpenSpec
- **THEN** system displays error message "✗ No openspec directory found"
### Requirement: Summary Section
The dashboard SHALL display a summary section with key project metrics.
#### Scenario: Complete summary display
- **WHEN** dashboard is rendered with specs and changes
- **THEN** system shows total number of specifications and requirements
- **AND** shows number of active changes in progress
- **AND** shows number of completed changes
- **AND** shows overall task progress percentage
#### Scenario: Empty project summary
- **WHEN** no specs or changes exist
- **THEN** summary shows zero counts for all metrics
### Requirement: Active Changes Display
The dashboard SHALL show active changes with visual progress indicators.
#### Scenario: Active changes with progress bars
- **WHEN** there are in-progress changes with tasks
- **THEN** system displays each change with change name left-aligned
- **AND** visual progress bar using Unicode characters
- **AND** percentage completion on the right
#### Scenario: No active changes
- **WHEN** all changes are completed or no changes exist
- **THEN** active changes section is omitted from display
### Requirement: Completed Changes Display
The dashboard SHALL list completed changes in a separate section.
#### Scenario: Completed changes listing
- **WHEN** there are completed changes (all tasks done)
- **THEN** system shows them with checkmark indicators in a dedicated section
#### Scenario: Mixed completion states
- **WHEN** some changes are complete and others active
- **THEN** system separates them into appropriate sections
### Requirement: Specifications Display
The dashboard SHALL display specifications sorted by requirement count.
#### Scenario: Specs listing with counts
- **WHEN** specifications exist in the project
- **THEN** system shows specs sorted by requirement count (descending) with count labels
#### Scenario: Specs with parsing errors
- **WHEN** a spec file cannot be parsed
- **THEN** system includes it with 0 requirement count
### Requirement: Visual Formatting
The dashboard SHALL use consistent visual formatting with colors and symbols.
#### Scenario: Color coding
- **WHEN** dashboard elements are displayed
- **THEN** system uses cyan for specification items
- **AND** yellow for active changes
- **AND** green for completed items
- **AND** dim gray for supplementary text
#### Scenario: Progress bar rendering
- **WHEN** displaying progress bars
- **THEN** system uses filled blocks (█) for completed portions and light blocks (░) for remaining
### Requirement: Error Handling
The view command SHALL handle errors gracefully.
#### Scenario: File system errors
- **WHEN** file system operations fail
- **THEN** system continues with available data and omits inaccessible items
#### Scenario: Invalid data structures
- **WHEN** specs or changes have invalid format
- **THEN** system skips invalid items and continues rendering
+8 -6
View File
@@ -1,6 +1,6 @@
{
"name": "openspec",
"version": "0.1.0-beta.0",
"name": "@fission-ai/openspec",
"version": "0.1.0",
"description": "AI-native system for spec-driven development",
"keywords": [
"openspec",
@@ -9,15 +9,16 @@
"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": {
@@ -45,12 +46,14 @@
"test:ui": "vitest --ui",
"test:coverage": "vitest --coverage",
"prepare": "pnpm run build",
"prepublishOnly": "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",
@@ -60,7 +63,6 @@
"@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
+15 -15
View File
@@ -5,9 +5,9 @@ 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 { ViewCommand } from '../core/view.js';
import { registerSpecCommand } from '../commands/spec.js';
import { ChangeCommand } from '../commands/change.js';
import { ValidateCommand } from '../commands/validate.js';
@@ -81,20 +81,6 @@ 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 items (changes by default). Use --specs to list specs.')
@@ -112,6 +98,20 @@ program
}
});
program
.command('view')
.description('Display an interactive dashboard of specs and changes')
.action(async () => {
try {
const viewCommand = new ViewCommand();
await viewCommand.execute('.');
} catch (error) {
console.log(); // Empty line for spacing
ora().fail(`Error: ${(error as Error).message}`);
process.exit(1);
}
});
// Change command with subcommands
const changeCmd = program
.command('change')
+13 -5
View File
@@ -241,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 [];
}
@@ -280,4 +288,4 @@ export class ChangeCommand {
console.error('Next steps:');
bullets.forEach(b => console.error(` ${b}`));
}
}
}
-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;
}
}
+14 -4
View File
@@ -16,6 +16,8 @@ Skip proposal for: bug fixes, typos, non-breaking updates
3. Read tasks.md for implementation checklist
4. Complete tasks one by one
5. Mark each task complete immediately: \`- [x]\`
6. Validate strictly: \`openspec validate [change] --strict\`
7. Approval gate: Do not start implementation until the proposal is approved
### Stage 3: Archiving
After deployment, use \`openspec archive [change]\` (add \`--skip-specs\` for tooling-only changes)
@@ -35,7 +37,6 @@ After deployment, use \`openspec archive [change]\` (add \`--skip-specs\` for to
openspec list # Active changes
openspec list --specs # Existing specifications
openspec show [item] # View details
openspec diff [change] # Show spec differences
openspec validate --strict # Validate thoroughly
openspec archive [change] # Archive after deployment
@@ -49,11 +50,20 @@ openspec show [change] --json --deltas-only
## Creating Changes
1. **Directory:** \`changes/[descriptive-name]/\`
1. **Directory:** \`changes/[change-id]/\`
- Change ID naming: kebab-case, verb-led (\`add-\`, \`update-\`, \`remove-\`, \`refactor-\`), unique (append \`-2\`, \`-3\` if needed)
2. **Files:**
- \`proposal.md\` - Why, what, impact
- \`tasks.md\` - Implementation checklist
- \`specs/[capability]/spec.md\` - Delta changes (ADDED/MODIFIED/REMOVED)
- \`design.md\` - Only if needed (cross-cutting, new deps/data model, security/perf/migration complexity, or high ambiguity)
- \`specs/[capability]/spec.md\` - Delta changes (ADDED/MODIFIED/REMOVED). For multiple capabilities, include multiple files.
3. **If ambiguous:** ask 1–2 clarifying questions before scaffolding
## Search Guidance
- Enumerate specs: \`openspec spec list --long\` (or \`--json\`)
- Enumerate changes: \`openspec list\`
- Show details: \`openspec show <spec-id> --type spec\`, \`openspec show <change-id> --json --deltas-only\`
- Full-text search (use ripgrep): \`rg -n "Requirement:|Scenario:" openspec/specs\`
## Critical: Scenario Format
@@ -92,4 +102,4 @@ Every requirement MUST have scenarios using \`#### Scenario:\` format.
- Don't use bullets or bold
**Debug:** \`openspec show [change] --json --deltas-only\`
`;
`;
+369 -452
View File
@@ -1,518 +1,435 @@
export const readmeTemplate = `# OpenSpec Instructions
This document provides instructions for AI coding assistants on how to use OpenSpec conventions for spec-driven development. Follow these rules precisely when working on OpenSpec-enabled projects.
Instructions for AI coding assistants using OpenSpec for spec-driven development.
## Core Principle
## TL;DR Quick Checklist
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
- Search existing work: \`openspec spec list --long\`, \`openspec list\` (use \`rg\` only for full-text search)
- Decide scope: new capability vs modify existing capability
- Pick a unique \`change-id\`: kebab-case, verb-led (\`add-\`, \`update-\`, \`remove-\`, \`refactor-\`)
- Scaffold: \`proposal.md\`, \`tasks.md\`, \`design.md\` (only if needed), and delta specs per affected capability
- Write deltas: use \`## ADDED|MODIFIED|REMOVED|RENAMED Requirements\`; include at least one \`#### Scenario:\` per requirement
- Validate: \`openspec validate [change-id] --strict\` and fix issues
- Request approval: Do not start implementation until proposal is approved
## Start Simple
## Three-Stage Workflow
**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 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
**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
Triggers (examples):
- "Help me create a change proposal"
- "Help me plan a change"
- "Help me create a proposal"
- "I want to create a spec proposal"
- "I want to create a spec"
When triggered, document the specific justification in your change proposal.
Loose matching guidance:
- Contains one of: \`proposal\`, \`change\`, \`spec\`
- With one of: \`create\`, \`plan\`, \`make\`, \`start\`, \`help\`
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
6. **Validate strictly** - Run \`openspec validate [change] --strict\` and address issues
7. **Approval gate** - Do not start implementation until the proposal is reviewed and approved
### 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
- If request is ambiguous, ask 1–2 clarifying questions before scaffolding
### Search Guidance
- Enumerate specs: \`openspec spec list --long\` (or \`--json\` for scripts)
- Enumerate changes: \`openspec list\` (or \`openspec change list --json\` - deprecated but available)
- Show details:
- Spec: \`openspec show <spec-id> --type spec\` (use \`--json\` for filters)
- Change: \`openspec show <change-id> --json --deltas-only\`
- Full-text search (use ripgrep): \`rg -n "Requirement:|Scenario:" openspec/specs\`
## 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-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; see criteria)
│ │ └── 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/[change-id]/\` (kebab-case, verb-led, unique)
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]
**Reason**: [Why removing]
**Migration**: [How to handle]
\`\`\`
If multiple capabilities are affected, create multiple delta files under \`changes/[change-id]/specs/<capability>/spec.md\`—one per capability.
## RENAMED Requirements
- FROM: \`### Requirement: Old Name\`
- TO: \`### Requirement: New Name\`
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
\`\`\`
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)
5. **Create design.md when needed:**
Create \`design.md\` if any of the following apply; otherwise omit it:
- Cross-cutting change (multiple services/modules) or a new architectural pattern
- New external dependency or significant data model changes
- Security, performance, or migration complexity
- Ambiguity that benefits from technical decisions before coding
### 4. Creating a Change Proposal
Minimal \`design.md\` skeleton:
\`\`\`markdown
## Context
[Background, constraints, stakeholders]
When a user requests a significant change:
## Goals / Non-Goals
- Goals: [...]
- Non-Goals: [...]
## Decisions
- Decision: [What and why]
- Alternatives considered: [Options + rationale]
## Risks / Trade-offs
- [Risk] → Mitigation
## Migration Plan
[Steps, rollback]
## Open Questions
- [...]
\`\`\`
## 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.
### Requirement Wording
- Use SHALL/MUST for normative requirements (avoid should/may unless intentionally non-normative)
### 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.
Example for RENAMED:
\`\`\`markdown
## RENAMED Requirements
- FROM: \`### Requirement: Login\`
- TO: \`### Requirement: User Authentication\`
\`\`\`
## 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
## Happy Path Script
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]/\`
\`\`\`bash
# 1) Explore current state
openspec spec list --long
openspec list
# Optional full-text search:
# rg -n "Requirement:|Scenario:" openspec/specs
# rg -n "^#|Requirement:" openspec/changes
### 6. Implementing Changes
# 2) Choose change id and scaffold
CHANGE=add-two-factor-auth
mkdir -p openspec/changes/$CHANGE/{specs/auth}
printf "## Why\\n...\\n\\n## What Changes\\n- ...\\n\\n## Impact\\n- ...\\n" > openspec/changes/$CHANGE/proposal.md
printf "## 1. Implementation\\n- [ ] 1.1 ...\\n" > openspec/changes/$CHANGE/tasks.md
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
# 3) Add deltas (example)
cat > openspec/changes/$CHANGE/specs/auth/spec.md << 'EOF'
## ADDED Requirements
### Requirement: Two-Factor Authentication
Users MUST provide a second factor during login.
**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
#### Scenario: OTP required
- **WHEN** valid credentials are provided
- **THEN** an OTP challenge is required
EOF
### 7. Updating Specs and Archiving After Deployment
# 4) Validate
openspec validate $CHANGE --strict
\`\`\`
**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\`
## Multi-Capability Example
This ensures changes are only archived when truly complete and deployed.
\`\`\`
openspec/changes/add-2fa-notify/
├── proposal.md
├── tasks.md
└── specs/
├── auth/
│ └── spec.md # ADDED: Two-Factor Authentication
└── notifications/
└── spec.md # ADDED: OTP email notification
\`\`\`
### 8. Types of Changes That Don't Require Specs
Some changes only affect development infrastructure and don't need specs:
- Initial project setup (package.json, tsconfig.json, etc.)
- Development tooling changes (linters, formatters, build tools)
- CI/CD configuration
- Development dependencies
For these changes:
1. Implement → Deploy → Mark tasks complete → Archive
2. Skip the "Update Specs" step entirely
### What Deserves a Spec?
Ask yourself:
- Is this a system capability that users or other systems interact with?
- Does it have ongoing behavior that needs documentation?
- Would a new developer need to understand this to work with the system?
If NO to all → No spec needed (likely just tooling/infrastructure)
## Understanding Specs vs Code
### Specs Document WHAT and WHY
auth/spec.md
\`\`\`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.
## ADDED Requirements
### Requirement: Two-Factor Authentication
...
\`\`\`
### Code Documents HOW
\`\`\`javascript
// Implementation details
const user = await db.users.findOne({ email });
const valid = await bcrypt.compare(password, user.hashedPassword);
notifications/spec.md
\`\`\`markdown
## ADDED Requirements
### Requirement: OTP Email Notification
...
\`\`\`
**Key Distinction**: Specs capture intent, constraints, and decisions that aren't obvious from code.
## Best Practices
## Common Scenarios
### Simplicity First
- Default to <100 lines of new code
- Single-file implementations until proven insufficient
- Avoid frameworks without clear justification
- Choose boring, proven patterns
### New Feature Request
\`\`\`
User: "Add password reset functionality"
### 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
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
### 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"
### Change ID Naming
- Use kebab-case, short and descriptive: \`add-two-factor-auth\`
- Prefer verb-led prefixes: \`add-\`, \`update-\`, \`remove-\`, \`refactor-\`
- Ensure uniqueness; if taken, append \`-2\`, \`-3\`, etc.
## 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
\`\`\`
### 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.
`;
+182
View File
@@ -0,0 +1,182 @@
import * as fs from 'fs';
import * as path from 'path';
import chalk from 'chalk';
import { getTaskProgressForChange, formatTaskStatus } from '../utils/task-progress.js';
import { MarkdownParser } from './parsers/markdown-parser.js';
export class ViewCommand {
async execute(targetPath: string = '.'): Promise<void> {
const openspecDir = path.join(targetPath, 'openspec');
if (!fs.existsSync(openspecDir)) {
console.error(chalk.red('No openspec directory found'));
process.exit(1);
}
console.log(chalk.bold('\nOpenSpec Dashboard\n'));
console.log('═'.repeat(60));
// Get changes and specs data
const changesData = await this.getChangesData(openspecDir);
const specsData = await this.getSpecsData(openspecDir);
// Display summary metrics
this.displaySummary(changesData, specsData);
// Display active changes
if (changesData.active.length > 0) {
console.log(chalk.bold.cyan('\nActive Changes'));
console.log('─'.repeat(60));
changesData.active.forEach(change => {
const progressBar = this.createProgressBar(change.progress.completed, change.progress.total);
const percentage = change.progress.total > 0
? Math.round((change.progress.completed / change.progress.total) * 100)
: 0;
console.log(
` ${chalk.yellow('◉')} ${chalk.bold(change.name.padEnd(30))} ${progressBar} ${chalk.dim(`${percentage}%`)}`
);
});
}
// Display completed changes
if (changesData.completed.length > 0) {
console.log(chalk.bold.green('\nCompleted Changes'));
console.log('─'.repeat(60));
changesData.completed.forEach(change => {
console.log(` ${chalk.green('✓')} ${change.name}`);
});
}
// Display specifications
if (specsData.length > 0) {
console.log(chalk.bold.blue('\nSpecifications'));
console.log('─'.repeat(60));
// Sort specs by requirement count (descending)
specsData.sort((a, b) => b.requirementCount - a.requirementCount);
specsData.forEach(spec => {
const reqLabel = spec.requirementCount === 1 ? 'requirement' : 'requirements';
console.log(
` ${chalk.blue('▪')} ${chalk.bold(spec.name.padEnd(30))} ${chalk.dim(`${spec.requirementCount} ${reqLabel}`)}`
);
});
}
console.log('\n' + '═'.repeat(60));
console.log(chalk.dim(`\nUse ${chalk.white('openspec list --changes')} or ${chalk.white('openspec list --specs')} for detailed views`));
}
private async getChangesData(openspecDir: string): Promise<{
active: Array<{ name: string; progress: { total: number; completed: number } }>;
completed: Array<{ name: string }>;
}> {
const changesDir = path.join(openspecDir, 'changes');
if (!fs.existsSync(changesDir)) {
return { active: [], completed: [] };
}
const active: Array<{ name: string; progress: { total: number; completed: number } }> = [];
const completed: Array<{ name: string }> = [];
const entries = fs.readdirSync(changesDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory() && entry.name !== 'archive') {
const progress = await getTaskProgressForChange(changesDir, entry.name);
if (progress.total === 0 || progress.completed === progress.total) {
completed.push({ name: entry.name });
} else {
active.push({ name: entry.name, progress });
}
}
}
// Sort alphabetically
active.sort((a, b) => a.name.localeCompare(b.name));
completed.sort((a, b) => a.name.localeCompare(b.name));
return { active, completed };
}
private async getSpecsData(openspecDir: string): Promise<Array<{ name: string; requirementCount: number }>> {
const specsDir = path.join(openspecDir, 'specs');
if (!fs.existsSync(specsDir)) {
return [];
}
const specs: Array<{ name: string; requirementCount: number }> = [];
const entries = fs.readdirSync(specsDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) {
const specFile = path.join(specsDir, entry.name, 'spec.md');
if (fs.existsSync(specFile)) {
try {
const content = fs.readFileSync(specFile, 'utf-8');
const parser = new MarkdownParser(content);
const spec = parser.parseSpec(entry.name);
const requirementCount = spec.requirements.length;
specs.push({ name: entry.name, requirementCount });
} catch (error) {
// If spec cannot be parsed, include with 0 count
specs.push({ name: entry.name, requirementCount: 0 });
}
}
}
}
return specs;
}
private displaySummary(
changesData: { active: any[]; completed: any[] },
specsData: any[]
): void {
const totalChanges = changesData.active.length + changesData.completed.length;
const totalSpecs = specsData.length;
const totalRequirements = specsData.reduce((sum, spec) => sum + spec.requirementCount, 0);
// Calculate total task progress
let totalTasks = 0;
let completedTasks = 0;
changesData.active.forEach(change => {
totalTasks += change.progress.total;
completedTasks += change.progress.completed;
});
changesData.completed.forEach(() => {
// Completed changes count as 100% done (we don't know exact task count)
// This is a simplification
});
console.log(chalk.bold('Summary:'));
console.log(` ${chalk.cyan('●')} Specifications: ${chalk.bold(totalSpecs)} specs, ${chalk.bold(totalRequirements)} requirements`);
console.log(` ${chalk.yellow('●')} Active Changes: ${chalk.bold(changesData.active.length)} in progress`);
console.log(` ${chalk.green('●')} Completed Changes: ${chalk.bold(changesData.completed.length)}`);
if (totalTasks > 0) {
const overallProgress = Math.round((completedTasks / totalTasks) * 100);
console.log(` ${chalk.magenta('●')} Task Progress: ${chalk.bold(`${completedTasks}/${totalTasks}`)} (${overallProgress}% complete)`);
}
}
private createProgressBar(completed: number, total: number, width: number = 20): string {
if (total === 0) return chalk.dim('─'.repeat(width));
const percentage = completed / total;
const filled = Math.round(percentage * width);
const empty = width - filled;
const filledBar = chalk.green('█'.repeat(filled));
const emptyBar = chalk.dim('░'.repeat(empty));
return `[${filledBar}${emptyBar}]`;
}
}
+12 -5
View File
@@ -5,10 +5,18 @@ export async function getActiveChangeIds(root: string = process.cwd()): Promise<
const changesPath = path.join(root, 'openspec', 'changes');
try {
const entries = await fs.readdir(changesPath, { withFileTypes: true });
return entries
.filter(entry => entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'archive')
.map(entry => entry.name)
.sort();
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 [];
}
@@ -35,4 +43,3 @@ export async function getSpecIds(root: string = process.cwd()): Promise<string[]
return result.sort();
}
@@ -1,4 +1,4 @@
import { describe, it, expect, beforeAll, beforeEach, afterEach } 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,9 +9,6 @@ describe('change show (interactive behavior)', () => {
const changesDir = path.join(testDir, 'openspec', 'changes');
const bin = path.join(projectRoot, 'bin', 'openspec.js');
beforeAll(() => {
execSync('pnpm -s build', { stdio: 'pipe' });
});
beforeEach(async () => {
await fs.mkdir(changesDir, { recursive: true });
@@ -1,4 +1,4 @@
import { describe, it, expect, beforeAll, beforeEach, afterEach } 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';
@@ -12,9 +12,6 @@ describe('change validate (interactive behavior)', () => {
const changesDir = path.join(testDir, 'openspec', 'changes');
const bin = path.join(projectRoot, 'bin', 'openspec.js');
beforeAll(() => {
execSync('pnpm -s build', { stdio: 'pipe' });
});
beforeEach(async () => {
await fs.mkdir(changesDir, { recursive: true });
+1 -4
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';
@@ -10,9 +10,6 @@ describe('top-level show command', () => {
const specsDir = path.join(testDir, 'openspec', 'specs');
const openspecBin = path.join(projectRoot, 'bin', 'openspec.js');
beforeAll(() => {
execSync('pnpm -s build', { stdio: 'pipe' });
});
beforeEach(async () => {
await fs.mkdir(changesDir, { recursive: true });
+1 -4
View File
@@ -1,4 +1,4 @@
import { describe, it, expect, beforeAll, beforeEach, afterEach } 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,9 +9,6 @@ describe('spec show (interactive behavior)', () => {
const specsDir = path.join(testDir, 'openspec', 'specs');
const bin = path.join(projectRoot, 'bin', 'openspec.js');
beforeAll(() => {
execSync('pnpm -s build', { stdio: 'pipe' });
});
beforeEach(async () => {
await fs.mkdir(specsDir, { recursive: true });
@@ -1,4 +1,4 @@
import { describe, it, expect, beforeAll, beforeEach, afterEach } 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,9 +9,6 @@ describe('spec validate (interactive behavior)', () => {
const specsDir = path.join(testDir, 'openspec', 'specs');
const bin = path.join(projectRoot, 'bin', 'openspec.js');
beforeAll(() => {
execSync('pnpm -s build', { stdio: 'pipe' });
});
beforeEach(async () => {
await fs.mkdir(specsDir, { recursive: true });
+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 });
@@ -1,4 +1,4 @@
import { describe, it, expect, beforeAll, beforeEach, afterEach } 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('validate command enriched human output', () => {
const changesDir = path.join(testDir, 'openspec', 'changes');
const bin = path.join(projectRoot, 'bin', 'openspec.js');
beforeAll(() => {
// Build once so the bin can resolve dist
try { execSync('pnpm -s build', { stdio: 'pipe' }); } catch {}
});
beforeEach(async () => {
await fs.mkdir(changesDir, { recursive: true });
+1 -4
View File
@@ -1,4 +1,4 @@
import { describe, it, expect, beforeAll, beforeEach, afterEach } 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';
@@ -10,9 +10,6 @@ describe('top-level validate command', () => {
const specsDir = path.join(testDir, 'openspec', 'specs');
const bin = path.join(projectRoot, 'bin', 'openspec.js');
beforeAll(() => {
execSync('pnpm -s build', { stdio: 'pipe' });
});
beforeEach(async () => {
await fs.mkdir(changesDir, { recursive: true });
+20 -5
View File
@@ -1,14 +1,29 @@
import { describe, it, expect, beforeAll } from 'vitest';
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { ChangeCommand } from '../../../src/commands/change.js';
// These tests assume the repository's own openspec/changes directory exists
// and contains at least one active change (e.g., add-change-commands)
import path from 'path';
import { promises as fs } from 'fs';
import os from 'os';
describe('ChangeCommand.list', () => {
let cmd: ChangeCommand;
let tempRoot: string;
let originalCwd: string;
beforeAll(() => {
beforeAll(async () => {
cmd = new ChangeCommand();
originalCwd = process.cwd();
tempRoot = path.join(os.tmpdir(), `openspec-change-command-list-${Date.now()}`);
const changeDir = path.join(tempRoot, 'openspec', 'changes', 'demo');
await fs.mkdir(changeDir, { recursive: true });
const proposal = `# Change: Demo\n\n## Why\nTest list.\n\n## What Changes\n- **auth:** Add requirement`;
await fs.writeFile(path.join(changeDir, 'proposal.md'), proposal, 'utf-8');
await fs.writeFile(path.join(changeDir, 'tasks.md'), '- [x] Task 1\n- [ ] Task 2\n', 'utf-8');
process.chdir(tempRoot);
});
afterAll(async () => {
process.chdir(originalCwd);
await fs.rm(tempRoot, { recursive: true, force: true });
});
it('returns JSON with expected shape', async () => {
@@ -1,34 +1,33 @@
import { describe, it, expect, beforeAll } from 'vitest';
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { ChangeCommand } from '../../../src/commands/change.js';
import path from 'path';
import { promises as fs } from 'fs';
async function findSingleActiveChange(root: string): Promise<string | undefined> {
const changesDir = path.join(root, 'openspec', 'changes');
try {
const entries = await fs.readdir(changesDir, { withFileTypes: true });
const names = entries
.filter((e) => e.isDirectory() && e.name !== 'archive')
.map((e) => e.name);
if (names.length === 1) return names[0];
return names.includes('add-change-commands') ? 'add-change-commands' : names[0];
} catch {
return undefined;
}
}
import os from 'os';
describe('ChangeCommand.show/validate', () => {
let cmd: ChangeCommand;
let changeName: string | undefined;
let changeName: string;
let tempRoot: string;
let originalCwd: string;
beforeAll(async () => {
cmd = new ChangeCommand();
changeName = await findSingleActiveChange(process.cwd());
originalCwd = process.cwd();
tempRoot = path.join(os.tmpdir(), `openspec-change-command-${Date.now()}`);
const changesDir = path.join(tempRoot, 'openspec', 'changes', 'sample-change');
await fs.mkdir(changesDir, { recursive: true });
const proposal = `# Change: Sample Change\n\n## Why\nConsistency in tests.\n\n## What Changes\n- **auth:** Add requirement`;
await fs.writeFile(path.join(changesDir, 'proposal.md'), proposal, 'utf-8');
process.chdir(tempRoot);
changeName = 'sample-change';
});
afterAll(async () => {
process.chdir(originalCwd);
await fs.rm(tempRoot, { recursive: true, force: true });
});
it('show --json prints JSON including deltas', async () => {
if (!changeName) return; // skip if no changes present
const logs: string[] = [];
const origLog = console.log;
try {
@@ -67,8 +66,6 @@ describe('ChangeCommand.show/validate', () => {
});
it('show --json --requirements-only returns minimal object with deltas (deprecated alias)', async () => {
if (!changeName) return; // skip if no changes present
const logs: string[] = [];
const origLog = console.log;
try {
@@ -93,8 +90,6 @@ describe('ChangeCommand.show/validate', () => {
});
it('validate --strict --json returns a report with valid boolean', async () => {
if (!changeName) return; // skip if no changes present
const logs: string[] = [];
const origLog = console.log;
try {
+4 -1
View File
@@ -4,6 +4,9 @@ export default defineConfig({
test: {
globals: true,
environment: 'node',
globalSetup: './vitest.setup.ts',
// Keep default pool settings; some tests rely on process.chdir,
// which is not supported in worker threads
include: ['test/**/*.test.ts'],
coverage: {
reporter: ['text', 'json', 'html'],
@@ -19,4 +22,4 @@ export default defineConfig({
testTimeout: 10000,
hookTimeout: 10000
}
});
});
+21
View File
@@ -0,0 +1,21 @@
import { execSync } from 'child_process';
import { existsSync } from 'fs';
import path from 'path';
// Run once before all tests
export async function setup() {
const distPath = path.join(process.cwd(), 'dist', 'cli', 'index.js');
if (!existsSync(distPath)) {
console.log('Building project before tests...');
try {
execSync('pnpm run build', {
stdio: 'inherit',
cwd: process.cwd()
});
} catch (error) {
console.error('Failed to build project:', error);
process.exit(1);
}
}
}