Compare commits

...
Author SHA1 Message Date
Tabish Bidiwale 8c1b580f03 feat(cli): add slash command support 2025-09-16 11:36:00 +10:00
Tabish Bidiwale 17e6f7166b docs(readme): merge 'What You Get' into 'Why OpenSpec?' 2025-09-16 10:33:29 +10:00
Tabish Bidiwale 931d10477e Merge pull request #59 from Fission-AI/codex/rename-agent-instruction-file-to-agents.md
chore(changes): propose agent file rename
2025-09-16 10:26:37 +10:00
Tabish Bidiwale e4548bcc58 Merge pull request #60 from Fission-AI/codex/add-custom-slash-command-support-for-openspec
docs: add slash command support proposal
2025-09-16 08:41:10 +10:00
Tabish Bidiwale 68fc049955 docs(changes): use proposal/apply/archive names and per-tool slash naming; add format examples and test guidance 2025-09-16 08:39:39 +10:00
Tabish Bidiwale 4874a16495 feat(validate): propose scope-aware change validation (validate only existing artifacts) 2025-09-16 08:15:39 +10:00
Tabish Bidiwale 3caabf86cf docs(readme): regenerate from template via openspec update 2025-09-16 07:42:52 +10:00
Tabish Bidiwale d4593e4a54 docs(readme): clarify ADDED vs MODIFIED and update README template 2025-09-16 07:40:35 +10:00
Tabish Bidiwale 0144ec2b1b fix(changes): use ADDED for slash command requirements in cli-init and cli-update 2025-09-16 07:40:21 +10:00
Tabish Bidiwale 98d90cc00d Merge pull request #61 from Fission-AI/view-command-proposal
feat: add openspec view dashboard command
2025-09-12 21:33:45 +10:00
Tabish Bidiwale 8eb7ebd7cd docs: detail slash command instructions 2025-09-12 18:25:40 +10:00
Tabish Bidiwale 9d7b44b722 chore(changes): propose agent file rename 2025-09-10 10:56:08 +10:00
26 changed files with 830 additions and 23 deletions
+6 -13
View File
@@ -31,20 +31,13 @@ OpenSpec ensures you and your AI assistant agree on what to build before any cod
**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
- **Deterministic, Predictable Output** - Clear specs lead to reliable, repeatable code generation
- **Team Alignment via Spec Reviews** - Everyone reviews intentions, not code surprises
- **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
- **Living Documentation** - Specs evolve with your code as a natural byproduct
- **Universal Tool Support** - Works with any AI assistant (Claude Code, Cursor, and more)
- **No API Keys Required** - Integrates through context rules, not external services
## How It Works
+23 -2
View File
@@ -40,20 +40,26 @@ Skip proposal for:
- Configuration changes
- Tests for existing behavior
**Workflow**
1. Review `openspec/project.md`, `openspec list`, and `openspec list --specs` to understand current context.
2. Choose a unique verb-led `change-id` and scaffold `proposal.md`, `tasks.md`, optional `design.md`, and spec deltas under `openspec/changes/<id>/`.
3. Draft spec deltas using `## ADDED|MODIFIED|REMOVED Requirements` with at least one `#### Scenario:` per requirement.
4. Run `openspec validate <id> --strict` and resolve any issues before sharing the proposal.
### 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
6. **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
- Run `openspec validate --strict` to confirm the archived change passes checks
## Before Any Task
@@ -87,6 +93,7 @@ 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
@@ -255,6 +262,19 @@ Every requirement MUST have at least one scenario.
Headers matched with `trim(header)` - whitespace ignored.
#### When to use ADDED vs MODIFIED
- ADDED: Introduces a new capability or sub-capability that can stand alone as a requirement. Prefer ADDED when the change is orthogonal (e.g., adding "Slash Command Configuration") rather than altering the semantics of an existing requirement.
- MODIFIED: Changes the behavior, scope, or acceptance criteria of an existing requirement. Always paste the full, updated requirement content (header + all scenarios). The archiver will replace the entire requirement with what you provide here; partial deltas will drop previous details.
- RENAMED: Use when only the name changes. If you also change behavior, use RENAMED (name) plus MODIFIED (content) referencing the new name.
Common pitfall: Using MODIFIED to add a new concern without including the previous text. This causes loss of detail at archive time. If you aren’t explicitly changing the existing requirement, add a new requirement under ADDED instead.
Authoring a MODIFIED requirement correctly:
1) Locate the existing requirement in `openspec/specs/<capability>/spec.md`.
2) Copy the entire requirement block (from `### Requirement: ...` through its scenarios).
3) Paste it under `## MODIFIED Requirements` and edit to reflect the new behavior.
4) Ensure the header text matches exactly (whitespace-insensitive) and keep at least one `#### Scenario:`.
Example for RENAMED:
```markdown
## RENAMED Requirements
@@ -425,6 +445,7 @@ 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
```
@@ -0,0 +1,119 @@
# Add Slash Command Support for Coding Agents
## Summary
- Enable OpenSpec to generate and update custom slash commands for supported coding agents (Claude Code and Cursor).
- Provide three slash commands aligned with OpenSpec's workflow: proposal (start a change proposal), apply (implement), and archive.
- Share slash command templating between agents to make future extensions simple.
## Motivation
Developers use different coding agents and editors. Having consistent slash commands across tools for the OpenSpec workflow reduces friction and ensures a standard way to trigger the workflow. Supporting both Claude Code and Cursor now lays a foundation for future agents that introduce slash command features.
## Proposal
1. During `openspec init`, when a user selects a supported tool, generate slash command configuration for three OpenSpec workflow stages:
- Claude (namespaced): `/openspec/proposal`, `/openspec/apply`, `/openspec/archive`.
- Cursor (flat, prefixed): `/openspec-proposal`, `/openspec-apply`, `/openspec-archive`.
- Semantics:
- Create – scaffold a change (ID, `proposal.md`, `tasks.md`, delta specs); validate strictly.
- Apply – implement an approved change; complete tasks; validate strictly.
- Archive – archive after deployment; update specs if needed.
- Each command file MUST embed concise, step-by-step instructions sourced from `openspec/README.md` (see Template Content section).
2. Store slash command files per tool:
- Claude Code: `.claude/commands/openspec/{proposal,apply,archive}.md`
- Cursor: `.cursor/commands/{openspec-proposal,openspec-apply,openspec-archive}.md`
- Ensure nested directories are created.
3. Command file format and metadata:
- Use Markdown with optional YAML frontmatter for tool metadata (name/title, description, category/tags) when supported by the tool.
- Place OpenSpec markers around the body only, never inside frontmatter.
- Keep the visible slash name, file name, and any frontmatter `name`/`id` consistently aligned (e.g., `proposal`, `openspec-proposal`).
- Namespacing: categorize these under “OpenSpec” and prefer unique IDs (e.g., `openspec-proposal`) to avoid collisions.
4. Centralize templates: define command bodies once and reuse across tools; apply minimal per-tool wrappers (frontmatter, categories, filenames).
5. During `openspec update`, refresh only existing slash command files (per-file basis) within markers; do not create missing files or new tools.
## Design Ideas
- Introduce `SlashCommandConfigurator` to manage multiple files per tool.
- Expose targets rather than a single `configFileName` (e.g., `getTargets(): Array<{ path: string; kind: 'slash'; id: string }>`).
- Provide `generateAll(projectPath, openspecDir)` for init and `updateExisting(projectPath, openspecDir)` for update.
- Per-tool adapters add only frontmatter and pathing; bodies come from shared templates.
- Templates live in `TemplateManager` with helpers that extract concise, authoritative snippets from `openspec/README.md`.
- Update flow logs per-file results so users see exactly which slash files were refreshed.
### Marker Placement
- Markers MUST wrap only the Markdown body contents:
- Frontmatter (if present) goes first.
- Then `<!-- OPENSPEC:START -->` … body … `<!-- OPENSPEC:END -->`.
- Avoid inserting markers into the YAML block to prevent parse errors.
### Idempotency and Creation Rules
- `init`: create all three files for the chosen tool(s) once; subsequent `init` runs are no-ops for existing files.
- `update`: refresh only files that exist; skip missing ones without creating new files.
- Directory creation for `.claude/commands/openspec/` and `.cursor/commands/` is the configurator’s responsibility.
### Command Naming & UX
- Claude Code: use namespacing in the slash itself for readability and grouping: `/openspec/proposal`, `/openspec/apply`, `/openspec/archive`.
- Cursor: use flat names with an `openspec-` prefix: `/openspec-proposal`, `/openspec-apply`, `/openspec-archive`. Group via `category: OpenSpec` when supported.
- Consistency: align file names, visible slash names, and any frontmatter `id` (e.g., `id: openspec-apply`).
- Migration: do not rename existing commands during `update`; apply new naming only on `init` (or via an explicit migrate step).
## Open Questions
- Validate exact metadata/frontmatter supported by each tool version; if unsupported, omit frontmatter and ship Markdown body only.
- Confirm the final Cursor command file location for the targeted versions; fall back to Markdown-only if Cursor does not parse frontmatter.
- Evaluate additional commands beyond the initial three (e.g., `/show-change`, `/validate-all`) based on user demand.
## Alternatives
- Hard-code slash command text per tool (rejected: duplicates content; increases maintenance).
- Delay Cursor support until its config stabilizes (partial accept): gate Cursor behind a feature flag until verified in real environments.
## Risks
- Tool configuration formats may change, requiring updates to wrappers/frontmatter.
- Incorrect paths or categories can hide commands; add path existence checks and clear logging.
- Marker misuse (inside frontmatter) can break parsing; enforce placement rules in tests.
## Future Work
- Support additional editors/agents that expose slash command APIs.
- Allow users to customize command names and categories during `openspec init`.
- Provide a dedicated command to regenerate slash commands without running full `update`.
## File Format Examples
The following examples illustrate expected structure. If a tool does not support frontmatter, omit the YAML block and keep only the markers + body.
### Claude Code: `.claude/commands/openspec/proposal.md`
```markdown
---
name: OpenSpec: Proposal
description: Scaffold a new OpenSpec change and validate strictly.
category: OpenSpec
tags: [openspec, change]
---
<!-- OPENSPEC:START -->
...command body from shared template...
<!-- OPENSPEC:END -->
```
Slash invocation: `/openspec/proposal` (namespaced)
### Cursor: `.cursor/commands/openspec-proposal.md`
```markdown
---
name: /openspec-proposal
id: openspec-proposal
category: OpenSpec
description: Scaffold a new OpenSpec change and validate strictly.
---
<!-- OPENSPEC:START -->
...command body from shared template...
<!-- OPENSPEC:END -->
```
Slash invocation: `/openspec-proposal` (flat, prefixed)
## Template Content
Templates should be brief, actionable, and sourced from `openspec/README.md` to avoid duplication. Each command body includes:
- Guardrails: ask 1–2 clarifying questions if needed; follow minimal-complexity rules; use `pnpm` for Node projects.
- Step list tailored to the workflow stage (proposal, apply, archive), including strict validation commands.
- Pointers to `openspec show`, `openspec list`, and troubleshooting tips when validation fails.
## Testing Strategy
- Golden snapshots for generated files per tool (frontmatter + markers + body).
- Partial presence tests: if 1–2 files exist, `update` only refreshes those and does not create missing ones.
- Marker placement tests: ensure markers never appear inside frontmatter; cover missing/duplicated marker recovery behavior.
- Logging tests: `update` reports per-file updates for slash commands.
@@ -0,0 +1,15 @@
## ADDED Requirements
### Requirement: Slash Command Configuration
The init command SHALL generate slash command files for supported editors using shared templates.
#### Scenario: Generating slash commands for Claude Code
- **WHEN** the user selects Claude Code during initialization
- **THEN** create `.claude/commands/openspec/proposal.md`, `.claude/commands/openspec/apply.md`, and `.claude/commands/openspec/archive.md`
- **AND** populate each file from shared templates so command text matches other tools
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
#### Scenario: Generating slash commands for Cursor
- **WHEN** the user selects Cursor during initialization
- **THEN** create `.cursor/commands/openspec-proposal.md`, `.cursor/commands/openspec-apply.md`, and `.cursor/commands/openspec-archive.md`
- **AND** populate each file from shared templates so command text matches other tools
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
@@ -0,0 +1,17 @@
## ADDED Requirements
### Requirement: Slash Command Updates
The update command SHALL refresh existing slash command files for configured tools without creating new ones.
#### Scenario: Updating slash commands for Claude Code
- **WHEN** `.claude/commands/openspec/` contains `proposal.md`, `apply.md`, and `archive.md`
- **THEN** refresh each file using shared templates
- **AND** ensure templates include instructions for the relevant workflow stage
#### Scenario: Updating slash commands for Cursor
- **WHEN** `.cursor/commands/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
- **THEN** refresh each file using shared templates
- **AND** ensure templates include instructions for the relevant workflow stage
#### Scenario: Missing slash command file
- **WHEN** a tool lacks a slash command file
- **THEN** do not create a new file during update
@@ -0,0 +1,16 @@
# Implementation Tasks
## 1. Templates and Configurators
- [x] 1.1 Create shared templates for the Proposal, Apply, and Archive commands with instructions for each workflow stage from `openspec/README.md`.
- [x] 1.2 Implement a `SlashCommandConfigurator` base and tool-specific configurators for Claude Code and Cursor.
## 2. Claude Code Integration
- [x] 2.1 Generate `.claude/commands/openspec/{proposal,apply,archive}.md` during `openspec init` using shared templates.
- [x] 2.2 Update existing `.claude/commands/openspec/*` files during `openspec update`.
## 3. Cursor Integration
- [x] 3.1 Generate `.cursor/commands/{openspec-proposal,openspec-apply,openspec-archive}.md` during `openspec init` using shared templates.
- [x] 3.2 Update existing `.cursor/commands/*` files during `openspec update`.
## 4. Verification
- [x] 4.1 Add tests verifying slash command files are created and updated correctly.
@@ -0,0 +1,12 @@
## Why
Validation currently errors on changes without spec deltas, even when the change is intentionally proposal-only or tooling-only. This creates false negatives and noisy CI.
## What Changes
- Make change validation scope-aware: validate only artifacts that exist.
- Only error on "No deltas found" if spec delta files exist but parse to zero deltas.
- Keep archive stricter: if specs exist but parse to zero deltas, fail; allow `--skip-specs` for tooling-only changes.
## Impact
- Affected specs: cli-validate
- Affected code: `src/commands/validate.ts`, `src/core/validation/validator.ts`
@@ -0,0 +1,25 @@
## ADDED Requirements
### Requirement: Scope-Aware Change Validation
The validator SHALL validate only artifacts that exist for a change, avoiding errors for proposal-only or tooling-only changes.
#### Scenario: Proposal-only change
- **WHEN** a change contains `proposal.md` but has no `specs/` directory or contains no `*/spec.md` files
- **THEN** validate the proposal (Why/What sections)
- **AND** do not require or validate spec deltas
#### Scenario: Delta validation when specs exist
- **WHEN** a change contains one or more `specs/<capability>/spec.md` files
- **THEN** validate delta-formatted specs with existing rules (SHALL/MUST, scenarios, duplicates, conflicts)
## MODIFIED Requirements
### Requirement: Validation SHALL provide actionable remediation steps
Validation output SHALL include specific guidance to fix each error, including expected structure, example headers, and suggested commands to verify fixes.
#### Scenario: No deltas found in change
- **WHEN** validating a change that contains `specs/` with one or more `*/spec.md` files but the parser finds zero deltas
- **THEN** show error "No deltas found" with guidance:
- Ensure `openspec/changes/{id}/specs/` has `.md` files that include delta headers
- Use delta headers: `## ADDED Requirements`, `## MODIFIED Requirements`, `## REMOVED Requirements`, `## RENAMED Requirements`
- Each requirement must include at least one `#### Scenario:` block
- Try: `openspec change show {id} --json --deltas-only` to inspect parsed deltas
@@ -0,0 +1,16 @@
## 1. Validator changes
- [ ] 1.1 Change `validateChangeDeltaSpecs` to only emit "Change must have at least one delta" when `specs/` exists and contains at least one `*/spec.md` but parsed total deltas is 0
- [ ] 1.2 Return valid (no error) when `specs/` directory is missing or has no `spec.md` files
## 2. CLI changes
- [ ] 2.1 In bulk validation, keep current behavior (call delta validator). Behavior remains correct after 1.1
- [ ] 2.2 Add a short INFO log in human-readable mode when a change has no `specs/` (optional)
## 3. Documentation
- [ ] 3.1 Update README and template: "Validation checks only existing artifacts. Proposal-only changes are valid without spec deltas."
## 4. Tests
- [ ] 4.1 Add test: proposal-only change passes validation without deltas
- [ ] 4.2 Add test: specs present but zero parsed deltas → ERROR
- [ ] 4.3 Add test: specs present with proper deltas → valid
@@ -0,0 +1,29 @@
# Update Agent Instruction File Name
## Problem
The agent instructions live in `openspec/README.md`, which clashes with conventional project README usage and creates confusion for tooling and contributors.
## Solution
Rename the agent instruction file to `openspec/AGENTS.md` and update OpenSpec tooling to use the new filename:
- `openspec init` generates `AGENTS.md` instead of `README.md`
- Templates and code reference `AGENTS.md`
- Specifications and documentation are updated accordingly
## Benefits
- Clear separation from project documentation
- Consistent naming with other agent instruction files
- Simplifies tooling and project onboarding
## Implementation
- Rename instruction file and template
- Update CLI commands (`init`, `update`) to read/write `AGENTS.md`
- Adjust specs and documentation to reference the new path
## Risks
- Existing projects may still rely on `README.md`
- Tooling may miss lingering references to the old filename
## Success Metrics
- `openspec init` creates `openspec/AGENTS.md`
- `openspec update` refreshes `AGENTS.md`
- All specs reference `openspec/AGENTS.md`
@@ -0,0 +1,36 @@
## MODIFIED Requirements
### Requirement: Directory Creation
The command SHALL create the complete OpenSpec directory structure with all required directories and files.
#### Scenario: Creating OpenSpec structure
- **WHEN** `openspec init` is executed
- **THEN** create the following directory structure:
```
openspec/
├── project.md
├── AGENTS.md
├── specs/
└── changes/
└── archive/
```
### Requirement: File Generation
The command SHALL generate required template files with appropriate content for immediate use.
#### Scenario: Generating template files
- **WHEN** initializing OpenSpec
- **THEN** generate `AGENTS.md` containing complete OpenSpec instructions for AI assistants
- **AND** generate `project.md` with project context template
### Requirement: AI Tool Configuration Details
#### Scenario: Creating new CLAUDE.md
- **WHEN** CLAUDE.md does not exist
- **THEN** create new file with OpenSpec content wrapped in markers including reference to `@openspec/AGENTS.md`
### Requirement: Success Output
#### Scenario: Displaying success message
- **WHEN** initialization completes successfully
- **THEN** include prompt: "Please explain the OpenSpec workflow from openspec/AGENTS.md and how I should work with you on this project"
@@ -0,0 +1,22 @@
## MODIFIED Requirements
### Requirement: Update Behavior
The update command SHALL update OpenSpec instruction files to the latest templates in a team-friendly manner.
#### Scenario: Running update command
- **WHEN** a user runs `openspec update`
- **THEN** replace `openspec/AGENTS.md` with the latest template
### Requirement: File Handling
The update command SHALL handle file updates in a predictable and safe manner.
#### Scenario: Updating files
- **WHEN** updating files
- **THEN** completely replace `openspec/AGENTS.md` with the latest template
### Requirement: Core Files Always Updated
The update command SHALL always update the core OpenSpec files and display an ASCII-safe success message.
#### Scenario: Successful update
- **WHEN** the update completes successfully
- **THEN** replace `openspec/AGENTS.md` with the latest template
@@ -0,0 +1,27 @@
## MODIFIED Requirements
### Requirement: Project Structure
An OpenSpec project SHALL maintain a consistent directory structure for specifications and changes.
#### Scenario: Initializing project structure
- **WHEN** an OpenSpec project is initialized
- **THEN** it SHALL have this structure:
```
openspec/
├── project.md # Project-specific context
├── AGENTS.md # AI assistant instructions
├── specs/ # Current deployed capabilities
│ └── [capability]/ # Single, focused capability
│ ├── spec.md # WHAT and WHY
│ └── design.md # HOW (optional, for established patterns)
└── changes/ # Proposed changes
├── [change-name]/ # Descriptive change identifier
│ ├── proposal.md # Why, what, and impact
│ ├── tasks.md # Implementation checklist
│ ├── design.md # Technical decisions (optional)
│ └── specs/ # Complete future state
│ └── [capability]/
│ └── spec.md # Clean markdown (no diff syntax)
└── archive/ # Completed changes
└── YYYY-MM-DD-[name]/
```
@@ -0,0 +1,22 @@
# Update Agent Instruction File Name - Tasks
## 1. Rename Instruction File
- [ ] Rename `openspec/README.md` to `openspec/AGENTS.md`
- [ ] Update root references to new path
## 2. Update Templates
- [ ] Rename `src/core/templates/readme-template.ts` to `agents-template.ts`
- [ ] Update exported constant from `readmeTemplate` to `agentsTemplate`
## 3. Adjust CLI Commands
- [ ] Modify `openspec init` to generate `AGENTS.md`
- [ ] Update `openspec update` to refresh `AGENTS.md`
- [ ] Ensure CLAUDE.md markers link to `@openspec/AGENTS.md`
## 4. Update Specifications
- [ ] Modify `cli-init` spec to reference `AGENTS.md`
- [ ] Modify `cli-update` spec to reference `AGENTS.md`
- [ ] Modify `openspec-conventions` spec to include `AGENTS.md` in project structure
## 5. Validation
- [ ] `pnpm test`
+2 -2
View File
@@ -11,7 +11,7 @@ export const OPENSPEC_MARKERS = {
export const AI_TOOLS = [
{ name: 'Claude Code', value: 'claude', available: true },
{ name: 'Cursor', value: 'cursor', available: false },
{ name: 'Cursor', value: 'cursor', available: true },
{ name: 'Aider', value: 'aider', available: false },
{ name: 'Continue', value: 'continue', available: false }
];
];
+85
View File
@@ -0,0 +1,85 @@
import path from 'path';
import { FileSystemUtils } from '../../../utils/file-system.js';
import { TemplateManager, SlashCommandId } from '../../templates/index.js';
import { OPENSPEC_MARKERS } from '../../config.js';
export interface SlashCommandTarget {
id: SlashCommandId;
path: string;
kind: 'slash';
}
const ALL_COMMANDS: SlashCommandId[] = ['proposal', 'apply', 'archive'];
export abstract class SlashCommandConfigurator {
abstract readonly toolId: string;
abstract readonly isAvailable: boolean;
getTargets(): SlashCommandTarget[] {
return ALL_COMMANDS.map((id) => ({
id,
path: this.getRelativePath(id),
kind: 'slash'
}));
}
async generateAll(projectPath: string, _openspecDir: string): Promise<string[]> {
const createdOrUpdated: string[] = [];
for (const target of this.getTargets()) {
const body = TemplateManager.getSlashCommandBody(target.id).trim();
const filePath = path.join(projectPath, target.path);
if (await FileSystemUtils.fileExists(filePath)) {
await this.updateBody(filePath, body);
} else {
const frontmatter = this.getFrontmatter(target.id);
const sections: string[] = [];
if (frontmatter) {
sections.push(frontmatter.trim());
}
sections.push(`${OPENSPEC_MARKERS.start}\n${body}\n${OPENSPEC_MARKERS.end}`);
const content = sections.join('\n') + '\n';
await FileSystemUtils.writeFile(filePath, content);
}
createdOrUpdated.push(target.path);
}
return createdOrUpdated;
}
async updateExisting(projectPath: string, _openspecDir: string): Promise<string[]> {
const updated: string[] = [];
for (const target of this.getTargets()) {
const filePath = path.join(projectPath, target.path);
if (await FileSystemUtils.fileExists(filePath)) {
const body = TemplateManager.getSlashCommandBody(target.id).trim();
await this.updateBody(filePath, body);
updated.push(target.path);
}
}
return updated;
}
protected abstract getRelativePath(id: SlashCommandId): string;
protected abstract getFrontmatter(id: SlashCommandId): string | undefined;
private async updateBody(filePath: string, body: string): Promise<void> {
const content = await FileSystemUtils.readFile(filePath);
const startIndex = content.indexOf(OPENSPEC_MARKERS.start);
const endIndex = content.indexOf(OPENSPEC_MARKERS.end);
if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) {
throw new Error(`Missing OpenSpec markers in ${filePath}`);
}
const before = content.slice(0, startIndex + OPENSPEC_MARKERS.start.length);
const after = content.slice(endIndex);
const updatedContent = `${before}\n${body}\n${after}`;
await FileSystemUtils.writeFile(filePath, updatedContent);
}
}
+42
View File
@@ -0,0 +1,42 @@
import { SlashCommandConfigurator } from './base.js';
import { SlashCommandId } from '../../templates/index.js';
const FILE_PATHS: Record<SlashCommandId, string> = {
proposal: '.claude/commands/openspec/proposal.md',
apply: '.claude/commands/openspec/apply.md',
archive: '.claude/commands/openspec/archive.md'
};
const FRONTMATTER: Record<SlashCommandId, string> = {
proposal: `---
name: OpenSpec: Proposal
description: Scaffold a new OpenSpec change and validate strictly.
category: OpenSpec
tags: [openspec, change]
---`,
apply: `---
name: OpenSpec: Apply
description: Implement an approved OpenSpec change and keep tasks in sync.
category: OpenSpec
tags: [openspec, apply]
---`,
archive: `---
name: OpenSpec: Archive
description: Archive a deployed OpenSpec change and update specs.
category: OpenSpec
tags: [openspec, archive]
---`
};
export class ClaudeSlashCommandConfigurator extends SlashCommandConfigurator {
readonly toolId = 'claude';
readonly isAvailable = true;
protected getRelativePath(id: SlashCommandId): string {
return FILE_PATHS[id];
}
protected getFrontmatter(id: SlashCommandId): string {
return FRONTMATTER[id];
}
}
+42
View File
@@ -0,0 +1,42 @@
import { SlashCommandConfigurator } from './base.js';
import { SlashCommandId } from '../../templates/index.js';
const FILE_PATHS: Record<SlashCommandId, string> = {
proposal: '.cursor/commands/openspec-proposal.md',
apply: '.cursor/commands/openspec-apply.md',
archive: '.cursor/commands/openspec-archive.md'
};
const FRONTMATTER: Record<SlashCommandId, string> = {
proposal: `---
name: /openspec-proposal
id: openspec-proposal
category: OpenSpec
description: Scaffold a new OpenSpec change and validate strictly.
---`,
apply: `---
name: /openspec-apply
id: openspec-apply
category: OpenSpec
description: Implement an approved OpenSpec change and keep tasks in sync.
---`,
archive: `---
name: /openspec-archive
id: openspec-archive
category: OpenSpec
description: Archive a deployed OpenSpec change and update specs.
---`
};
export class CursorSlashCommandConfigurator extends SlashCommandConfigurator {
readonly toolId = 'cursor';
readonly isAvailable = true;
protected getRelativePath(id: SlashCommandId): string {
return FILE_PATHS[id];
}
protected getFrontmatter(id: SlashCommandId): string {
return FRONTMATTER[id];
}
}
+27
View File
@@ -0,0 +1,27 @@
import { SlashCommandConfigurator } from './base.js';
import { ClaudeSlashCommandConfigurator } from './claude.js';
import { CursorSlashCommandConfigurator } from './cursor.js';
export class SlashCommandRegistry {
private static configurators: Map<string, SlashCommandConfigurator> = new Map();
static {
const claude = new ClaudeSlashCommandConfigurator();
const cursor = new CursorSlashCommandConfigurator();
this.configurators.set(claude.toolId, claude);
this.configurators.set(cursor.toolId, cursor);
}
static register(configurator: SlashCommandConfigurator): void {
this.configurators.set(configurator.toolId, configurator);
}
static get(toolId: string): SlashCommandConfigurator | undefined {
return this.configurators.get(toolId);
}
static getAll(): SlashCommandConfigurator[] {
return Array.from(this.configurators.values());
}
}
+7 -1
View File
@@ -4,6 +4,7 @@ import ora from 'ora';
import { FileSystemUtils } from '../utils/file-system.js';
import { TemplateManager, ProjectContext } from './templates/index.js';
import { ToolRegistry } from './configurators/registry.js';
import { SlashCommandRegistry } from './configurators/slash/registry.js';
import { OpenSpecConfig, AI_TOOLS, OPENSPEC_DIR_NAME } from './config.js';
export class InitCommand {
@@ -105,6 +106,11 @@ export class InitCommand {
if (configurator && configurator.isAvailable) {
await configurator.configure(projectPath, openspecDir);
}
const slashConfigurator = SlashCommandRegistry.get(toolId);
if (slashConfigurator && slashConfigurator.isAvailable) {
await slashConfigurator.generateAll(projectPath, openspecDir);
}
}
}
@@ -130,4 +136,4 @@ export class InitCommand {
console.log(' and how I should work with you on this project"');
console.log('────────────────────────────────────────────────────────────\n');
}
}
}
+7 -1
View File
@@ -1,6 +1,7 @@
import { readmeTemplate } from './readme-template.js';
import { projectTemplate, ProjectContext } from './project-template.js';
import { claudeTemplate } from './claude-template.js';
import { getSlashCommandBody, SlashCommandId } from './slash-command-templates.js';
export interface Template {
path: string;
@@ -24,6 +25,11 @@ export class TemplateManager {
static getClaudeTemplate(): string {
return claudeTemplate;
}
static getSlashCommandBody(id: SlashCommandId): string {
return getSlashCommandBody(id);
}
}
export { ProjectContext } from './project-template.js';
export { ProjectContext } from './project-template.js';
export type { SlashCommandId } from './slash-command-templates.js';
+21 -2
View File
@@ -40,20 +40,26 @@ Skip proposal for:
- Configuration changes
- Tests for existing behavior
**Workflow**
1. Review \`openspec/project.md\`, \`openspec list\`, and \`openspec list --specs\` to understand current context.
2. Choose a unique verb-led \`change-id\` and scaffold \`proposal.md\`, \`tasks.md\`, optional \`design.md\`, and spec deltas under \`openspec/changes/<id>/\`.
3. Draft spec deltas using \`## ADDED|MODIFIED|REMOVED Requirements\` with at least one \`#### Scenario:\` per requirement.
4. Run \`openspec validate <id> --strict\` and resolve any issues before sharing the proposal.
### 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
6. **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
- Run \`openspec validate --strict\` to confirm the archived change passes checks
## Before Any Task
@@ -256,6 +262,19 @@ Every requirement MUST have at least one scenario.
Headers matched with \`trim(header)\` - whitespace ignored.
#### When to use ADDED vs MODIFIED
- ADDED: Introduces a new capability or sub-capability that can stand alone as a requirement. Prefer ADDED when the change is orthogonal (e.g., adding "Slash Command Configuration") rather than altering the semantics of an existing requirement.
- MODIFIED: Changes the behavior, scope, or acceptance criteria of an existing requirement. Always paste the full, updated requirement content (header + all scenarios). The archiver will replace the entire requirement with what you provide here; partial deltas will drop previous details.
- RENAMED: Use when only the name changes. If you also change behavior, use RENAMED (name) plus MODIFIED (content) referencing the new name.
Common pitfall: Using MODIFIED to add a new concern without including the previous text. This causes loss of detail at archive time. If you aren’t explicitly changing the existing requirement, add a new requirement under ADDED instead.
Authoring a MODIFIED requirement correctly:
1) Locate the existing requirement in \`openspec/specs/<capability>/spec.md\`.
2) Copy the entire requirement block (from \`### Requirement: ...\` through its scenarios).
3) Paste it under \`## MODIFIED Requirements\` and edit to reflect the new behavior.
4) Ensure the header text matches exactly (whitespace-insensitive) and keep at least one \`#### Scenario:\`.
Example for RENAMED:
\`\`\`markdown
## RENAMED Requirements
@@ -0,0 +1,45 @@
export type SlashCommandId = 'proposal' | 'apply' | 'archive';
const baseGuardrails = `**Guardrails**
- Default to <100 lines of new code, single-file solutions, and avoid new frameworks unless OpenSpec data requires it.
- Use pnpm for Node.js tooling and keep changes scoped to the requested outcome.`;
const proposalGuardrails = `${baseGuardrails}\n- Ask up to two clarifying questions if the request is ambiguous before editing files.`;
const proposalSteps = `**Steps**
1. Review \`openspec/project.md\`, run \`openspec list\`, and \`openspec list --specs\` to understand current work and capabilities.
2. Choose a unique verb-led \`change-id\` and scaffold \`proposal.md\`, \`tasks.md\`, and optional \`design.md\` under \`openspec/changes/<id>/\`.
3. Draft spec deltas in \`changes/<id>/specs/\` using \`## ADDED|MODIFIED|REMOVED Requirements\` with at least one \`#### Scenario:\` per requirement.
4. Validate with \`openspec validate <id> --strict\` and resolve every issue before sharing the proposal.`;
const proposalReferences = `**Reference**
- Use \`openspec show <id> --json --deltas-only\` or \`openspec show <spec> --type spec\` to inspect details when validation fails.
- Search existing requirements with \`rg -n "Requirement:|Scenario:" openspec/specs\` before writing new ones.`;
const applySteps = `**Steps**
1. Read \`changes/<id>/proposal.md\`, \`design.md\` (if present), and \`tasks.md\` to confirm scope and acceptance criteria.
2. Work through tasks sequentially, keeping edits minimal and focused on the requested change.
3. Mark each task \`- [x]\` immediately after completing it to keep the checklist in sync.
4. Reference \`openspec list\` or \`openspec show <item>\` when additional context is required.`;
const applyReferences = `**Reference**
- Use \`openspec show <id> --json --deltas-only\` if you need additional context from the proposal while implementing.`;
const archiveSteps = `**Steps**
1. Confirm deployment is complete, then move \`changes/<id>/\` to \`changes/archive/YYYY-MM-DD-<id>/\`.
2. Update \`openspec/specs/\` to capture production behaviour, editing existing capabilities before creating new ones.
3. Run \`openspec archive <id> --skip-specs\` only for tooling-only work; otherwise ensure spec deltas are committed.
4. Re-run \`openspec validate --strict\` and review with \`openspec show <id>\` to verify archive changes.`;
const archiveReferences = `**Reference**
- Cross-check capabilities with \`openspec list --specs\` and resolve any outstanding validation issues before finishing.`;
export const slashCommandBodies: Record<SlashCommandId, string> = {
proposal: [proposalGuardrails, proposalSteps, proposalReferences].join('\n\n'),
apply: [baseGuardrails, applySteps, applyReferences].join('\n\n'),
archive: [baseGuardrails, archiveSteps, archiveReferences].join('\n\n')
};
export function getSlashCommandBody(id: SlashCommandId): string {
return slashCommandBodies[id];
}
+28
View File
@@ -3,6 +3,7 @@ import { FileSystemUtils } from '../utils/file-system.js';
import { OPENSPEC_DIR_NAME } from './config.js';
import { readmeTemplate } from './templates/readme-template.js';
import { ToolRegistry } from './configurators/registry.js';
import { SlashCommandRegistry } from './configurators/slash/registry.js';
export class UpdateCommand {
async execute(projectPath: string): Promise<void> {
@@ -21,8 +22,11 @@ export class UpdateCommand {
// 3. Update existing AI tool configuration files only
const configurators = ToolRegistry.getAll();
const slashConfigurators = SlashCommandRegistry.getAll();
let updatedFiles: string[] = [];
let failedFiles: string[] = [];
let updatedSlashFiles: string[] = [];
let failedSlashTools: string[] = [];
for (const configurator of configurators) {
const configFilePath = path.join(resolvedProjectPath, configurator.configFileName);
@@ -39,16 +43,40 @@ export class UpdateCommand {
}
}
for (const slashConfigurator of slashConfigurators) {
if (!slashConfigurator.isAvailable) {
continue;
}
try {
const updated = await slashConfigurator.updateExisting(resolvedProjectPath, openspecPath);
updatedSlashFiles = updatedSlashFiles.concat(updated);
} catch (error) {
failedSlashTools.push(slashConfigurator.toolId);
console.error(
`Failed to update slash commands for ${slashConfigurator.toolId}: ${error instanceof Error ? error.message : String(error)}`
);
}
}
// 4. Success message (ASCII-safe)
const messages: string[] = ['Updated OpenSpec instructions (README.md)'];
if (updatedFiles.length > 0) {
messages.push(`Updated AI tool files: ${updatedFiles.join(', ')}`);
}
if (updatedSlashFiles.length > 0) {
messages.push(`Updated slash commands: ${updatedSlashFiles.join(', ')}`);
}
if (failedFiles.length > 0) {
messages.push(`Failed to update: ${failedFiles.join(', ')}`);
}
if (failedSlashTools.length > 0) {
messages.push(`Failed slash command updates: ${failedSlashTools.join(', ')}`);
}
console.log(messages.join('\n'));
}
+54 -1
View File
@@ -86,6 +86,59 @@ describe('InitCommand', () => {
expect(updatedContent).toContain('Custom instructions here');
});
it('should create Claude slash command files with templates', async () => {
vi.mocked(prompts.select).mockResolvedValue('claude');
await initCommand.execute(testDir);
const claudeProposal = path.join(testDir, '.claude/commands/openspec/proposal.md');
const claudeApply = path.join(testDir, '.claude/commands/openspec/apply.md');
const claudeArchive = path.join(testDir, '.claude/commands/openspec/archive.md');
expect(await fileExists(claudeProposal)).toBe(true);
expect(await fileExists(claudeApply)).toBe(true);
expect(await fileExists(claudeArchive)).toBe(true);
const proposalContent = await fs.readFile(claudeProposal, 'utf-8');
expect(proposalContent).toContain('name: OpenSpec: Proposal');
expect(proposalContent).toContain('<!-- OPENSPEC:START -->');
expect(proposalContent).toContain('**Guardrails**');
const applyContent = await fs.readFile(claudeApply, 'utf-8');
expect(applyContent).toContain('name: OpenSpec: Apply');
expect(applyContent).toContain('Work through tasks sequentially');
const archiveContent = await fs.readFile(claudeArchive, 'utf-8');
expect(archiveContent).toContain('name: OpenSpec: Archive');
expect(archiveContent).toContain('openspec archive <id> --skip-specs');
});
it('should create Cursor slash command files with templates', async () => {
vi.mocked(prompts.select).mockResolvedValue('cursor');
await initCommand.execute(testDir);
const cursorProposal = path.join(testDir, '.cursor/commands/openspec-proposal.md');
const cursorApply = path.join(testDir, '.cursor/commands/openspec-apply.md');
const cursorArchive = path.join(testDir, '.cursor/commands/openspec-archive.md');
expect(await fileExists(cursorProposal)).toBe(true);
expect(await fileExists(cursorApply)).toBe(true);
expect(await fileExists(cursorArchive)).toBe(true);
const proposalContent = await fs.readFile(cursorProposal, 'utf-8');
expect(proposalContent).toContain('name: /openspec-proposal');
expect(proposalContent).toContain('<!-- OPENSPEC:END -->');
const applyContent = await fs.readFile(cursorApply, 'utf-8');
expect(applyContent).toContain('id: openspec-apply');
expect(applyContent).toContain('Work through tasks sequentially');
const archiveContent = await fs.readFile(cursorArchive, 'utf-8');
expect(archiveContent).toContain('name: /openspec-archive');
expect(archiveContent).toContain('openspec list --specs');
});
it('should throw error if OpenSpec already exists', async () => {
const openspecPath = path.join(testDir, 'openspec');
await fs.mkdir(openspecPath, { recursive: true });
@@ -180,4 +233,4 @@ async function directoryExists(dirPath: string): Promise<boolean> {
} catch {
return false;
}
}
}
+85 -1
View File
@@ -61,6 +61,37 @@ More content after.`;
consoleSpy.mockRestore();
});
it('should refresh existing Claude slash command files', async () => {
const proposalPath = path.join(testDir, '.claude/commands/openspec/proposal.md');
await fs.mkdir(path.dirname(proposalPath), { recursive: true });
const initialContent = `---
name: OpenSpec: Proposal
description: Old description
category: OpenSpec
tags: [openspec, change]
---
<!-- OPENSPEC:START -->
Old slash content
<!-- OPENSPEC:END -->`;
await fs.writeFile(proposalPath, initialContent);
const consoleSpy = vi.spyOn(console, 'log');
await updateCommand.execute(testDir);
const updated = await fs.readFile(proposalPath, 'utf-8');
expect(updated).toContain('name: OpenSpec: Proposal');
expect(updated).toContain('**Guardrails**');
expect(updated).toContain('Validate with `openspec validate <id> --strict`');
expect(updated).not.toContain('Old slash content');
expect(consoleSpy).toHaveBeenCalledWith(
'Updated OpenSpec instructions (README.md)\nUpdated slash commands: .claude/commands/openspec/proposal.md'
);
consoleSpy.mockRestore();
});
it('should not create CLAUDE.md if it does not exist', async () => {
// Ensure CLAUDE.md does not exist
const claudePath = path.join(testDir, 'CLAUDE.md');
@@ -73,6 +104,36 @@ More content after.`;
expect(fileExists).toBe(false);
});
it('should refresh existing Cursor slash command files', async () => {
const cursorPath = path.join(testDir, '.cursor/commands/openspec-apply.md');
await fs.mkdir(path.dirname(cursorPath), { recursive: true });
const initialContent = `---
name: /openspec-apply
id: openspec-apply
category: OpenSpec
description: Old description
---
<!-- OPENSPEC:START -->
Old body
<!-- OPENSPEC:END -->`;
await fs.writeFile(cursorPath, initialContent);
const consoleSpy = vi.spyOn(console, 'log');
await updateCommand.execute(testDir);
const updated = await fs.readFile(cursorPath, 'utf-8');
expect(updated).toContain('id: openspec-apply');
expect(updated).toContain('Work through tasks sequentially');
expect(updated).not.toContain('Old body');
expect(consoleSpy).toHaveBeenCalledWith(
'Updated OpenSpec instructions (README.md)\nUpdated slash commands: .cursor/commands/openspec-apply.md'
);
consoleSpy.mockRestore();
});
it('should handle no AI tool files present', async () => {
// Execute update command with no AI tool files
const consoleSpy = vi.spyOn(console, 'log');
@@ -89,6 +150,7 @@ More content after.`;
// that all existing files are updated in a single operation.
// For now, we test with just CLAUDE.md.
const claudePath = path.join(testDir, 'CLAUDE.md');
await fs.mkdir(path.dirname(claudePath), { recursive: true });
await fs.writeFile(claudePath, '<!-- OPENSPEC:START -->\nOld\n<!-- OPENSPEC:END -->');
const consoleSpy = vi.spyOn(console, 'log');
@@ -101,6 +163,28 @@ More content after.`;
consoleSpy.mockRestore();
});
it('should skip creating missing slash commands during update', async () => {
const proposalPath = path.join(testDir, '.claude/commands/openspec/proposal.md');
await fs.mkdir(path.dirname(proposalPath), { recursive: true });
await fs.writeFile(proposalPath, `---
name: OpenSpec: Proposal
description: Existing file
category: OpenSpec
tags: [openspec, change]
---
<!-- OPENSPEC:START -->
Old content
<!-- OPENSPEC:END -->`);
await updateCommand.execute(testDir);
const applyExists = await FileSystemUtils.fileExists(path.join(testDir, '.claude/commands/openspec/apply.md'));
const archiveExists = await FileSystemUtils.fileExists(path.join(testDir, '.claude/commands/openspec/archive.md'));
expect(applyExists).toBe(false);
expect(archiveExists).toBe(false);
});
it('should never create new AI tool files', async () => {
// Get all configurators
const configurators = ToolRegistry.getAll();
@@ -162,4 +246,4 @@ More content after.`;
consoleSpy.mockRestore();
errorSpy.mockRestore();
});
});
});