Compare commits

...
Author SHA1 Message Date
Tabish Bidiwale 8382ee8660 refactor: optimize tool state detection and improve code clarity
Address code review feedback:

1. **Parallelize tool state checks**: Changed from sequential `for` loop to
   `Promise.all()` for checking multiple tools simultaneously. This reduces
   I/O latency during extend mode initialization.

2. **Extract marker validation helper**: Created `fileHasMarkers()` helper
   function to eliminate code duplication between config file and slash
   command checks. Makes the logic clearer and more maintainable.

3. **Clarify slash command policy**: Added explicit comment that "at least
   one file with markers is sufficient" (not all required) for slash commands.
   This is correct because OpenSpec creates all files together - if any
   exists with markers, the tool was configured by OpenSpec.

4. **Simplify fresh init path**: Use `Object.fromEntries()` for cleaner
   initialization of all-false states.

Performance improvement: Extend mode now checks tools in parallel instead
of serially, reducing init time especially for projects with many tools.
2025-10-25 20:27:28 +11:00
Tabish Bidiwale 93326be3e9 fix: prevent false "already configured" detection for tools
Fixes #195

## Problem
Users with existing tool config files (like CLAUDE.md) would see those
tools marked as "already configured" even when running `openspec init`
for the first time. This caused confusion as users thought OpenSpec was
already set up when it wasn't.

Root causes:
1. Tool detection checked only for file existence, not OpenSpec ownership
2. Detection ran even in fresh projects without openspec/ folder

## Solution
Two-part fix:

1. **Conditional detection**: Only check tool configuration when in extend
   mode (when openspec/ directory already exists). Fresh initializations
   skip the check entirely, treating all tools as unconfigured.

2. **Marker-based validation**: For tools to be considered "configured by
   OpenSpec", their files must contain OpenSpec markers (<!-- OPENSPEC:START -->
   and <!-- OPENSPEC:END -->). For tools with both config files and slash
   commands (like Claude Code), BOTH must have markers.

## Changes
- Modified getExistingToolStates() to accept extendMode parameter
- Rewrote isToolConfigured() to verify OpenSpec markers in files
- Added OPENSPEC_MARKERS to imports
- Added 4 comprehensive test cases covering the new behavior

## Test Coverage
- Fresh init with existing CLAUDE.md → NOT shown as configured ✅
- Fresh init with existing slash commands → NOT shown as configured ✅
- Extend mode with OpenSpec files → shown as configured ✅
- Fresh init with global Codex prompts → NOT shown as configured ✅

All 240 tests pass.
2025-10-25 19:37:48 +11:00
Tabish Bidiwale 92b45462c6 fix: use change-id as fallback title instead of "Untitled Change" (#236)
* fix: use change-id as fallback title instead of "Untitled Change"

Fixes #225 by addressing mismatch between proposal template and title extraction:

- Updated proposal template to include `# Change: [description]` header
- Changed extractTitle fallback from "Untitled Change" to change-id
- Updated all extractTitle call sites to pass changeName parameter

This ensures both new and existing proposals display meaningful titles.

* refactor: make title extraction case-insensitive for "Change:"

Makes the extractTitle regex case-insensitive to handle variations like
"# change:" or "# CHANGE:" in addition to "# Change:".
2025-10-25 16:30:37 +11:00
Tabish Bidiwale 5ab438f5fd docs: add Crush to supported AI tools in README (#235)
Add Crush to the Native Slash Commands table in the README. Crush support was implemented in the codebase but was missing from the documentation.
2025-10-24 12:46:35 +11:00
github-actions[bot]andTabish Bidiwale 5855fa2353 chore(release): version packages (#228)
* Version Packages

* chore: trigger CI

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Tabish Bidiwale <tabishbidiwale@gmail.com>
2025-10-22 17:18:52 +11:00
Tabish Bidiwale 668a125d4d chore: add changeset for AI assistants support and validation fixes (#227)
* fix: manually merge and archive four completed OpenSpec changes

Merged and archived four concurrent changes that modified the same specs:
- add-cline-support: Added Cline AI tool configuration
- add-crush-support: Added Crush AI tool configuration
- add-factory-slash-commands: Added Factory Droid slash commands
- add-archive-command-arguments: Added archive command argument support

Changes to cli-init/spec.md:
- Added Cline and CodeBuddy Code configuration scenarios
- Added slash command scenarios for CodeBuddy Code, Cline, Crush, Factory Droid

Changes to cli-update/spec.md:
- Added update scenarios for CodeBuddy Code, Cline, Crush, Factory Droid
- Added Archive Command Argument Support requirement
- Modified OpenCode scenario to support $ARGUMENTS placeholder

This required manual intervention to prevent data loss from OpenSpec's
requirement-level replacement during archiving. The archive operations
initially overwrote scenarios from earlier changes, requiring restoration
of all missing content to preserve complete spec state.

All four changes successfully archived to openspec/changes/archive/.

* Add changeset for AI assistants support and validation fixes
2025-10-22 17:03:48 +11:00
Tabish Bidiwale fef961f6e3 fix: manually merge and archive four completed OpenSpec changes (#226)
Merged and archived four concurrent changes that modified the same specs:
- add-cline-support: Added Cline AI tool configuration
- add-crush-support: Added Crush AI tool configuration
- add-factory-slash-commands: Added Factory Droid slash commands
- add-archive-command-arguments: Added archive command argument support

Changes to cli-init/spec.md:
- Added Cline and CodeBuddy Code configuration scenarios
- Added slash command scenarios for CodeBuddy Code, Cline, Crush, Factory Droid

Changes to cli-update/spec.md:
- Added update scenarios for CodeBuddy Code, Cline, Crush, Factory Droid
- Added Archive Command Argument Support requirement
- Modified OpenCode scenario to support $ARGUMENTS placeholder

This required manual intervention to prevent data loss from OpenSpec's
requirement-level replacement during archiving. The archive operations
initially overwrote scenarios from earlier changes, requiring restoration
of all missing content to preserve complete spec state.

All four changes successfully archived to openspec/changes/archive/.
2025-10-22 16:07:28 +11:00
28 changed files with 475 additions and 99 deletions
+29
View File
@@ -1,5 +1,34 @@
# @fission-ai/openspec
## 0.13.0
### Minor Changes
- 668a125: Add support for multiple AI assistants and improve validation
This release adds support for several new AI coding assistants:
- CodeBuddy Code - AI-powered coding assistant
- CodeRabbit - AI code review assistant
- Cline - Claude-powered CLI assistant
- Crush AI - AI assistant platform
- Auggie (Augment CLI) - Code augmentation tool
New features:
- Archive slash command now supports arguments for more flexible workflows
Bug fixes:
- Delta spec validation now handles case-insensitive headers and properly detects empty sections
- Archive validation now correctly honors --no-validate flag and ignores metadata
Documentation improvements:
- Added VS Code dev container configuration for easier development setup
- Updated AGENTS.md with explicit change-id notation
- Enhanced slash commands documentation with restart notes
## 0.12.0
### Minor Changes
+2 -1
View File
@@ -91,9 +91,10 @@ These tools have built-in OpenSpec commands. Select the OpenSpec integration whe
| Tool | Commands |
|------|----------|
| **Claude Code** | `/openspec:proposal`, `/openspec:apply`, `/openspec:archive` |
| **CodeBuddy** | `/openspec:proposal`, `/openspec:apply`, `/openspec:archive` (`.codebuddy/commands/`) |
| **CodeBuddy Code (CLI)** | `/openspec:proposal`, `/openspec:apply`, `/openspec:archive` (`.codebuddy/commands/`) — see [docs](https://www.codebuddy.ai/cli) |
| **Cursor** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` |
| **Cline** | Rules in `.clinerules/` directory (`.clinerules/openspec-*.md`) |
| **Crush** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.crush/commands/openspec/`) |
| **Factory Droid** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.factory/commands/`) |
| **OpenCode** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` |
| **Kilo Code** | `/openspec-proposal.md`, `/openspec-apply.md`, `/openspec-archive.md` (`.kilocode/workflows/`) |
+1 -3
View File
@@ -49,9 +49,7 @@ _Outcome:_ We prevent data loss immediately while we work on a richer merge stor
- On conflict, write conflict markers inside the change delta (similar to Git) and require the author to hand-edit before re-running validation.
2. **Enrich validator messages.**
- `openspec validate` should flag unresolved conflict markers or fingerprint mismatches so errors appear early in the workflow.
3. **Improve diff tooling.**
- Extend `openspec diff` to compare change deltas against the live spec and highlight pending merges.
4. **Optional:** Offer a `--rewrite-scenarios` helper that merges bullet lists of scenarios to reduce manual editing noise.
3. **Optional:** Offer a `--rewrite-scenarios` helper that merges bullet lists of scenarios to reduce manual editing noise.
_Outcome:_ Contributors can safely reconcile their work with the latest spec before archiving, restoring true parallel development.
-2
View File
@@ -95,7 +95,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-id> [--yes|-y] # Archive after deployment (add --yes for non-interactive runs)
@@ -448,7 +447,6 @@ 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-id> [--yes|-y] # Mark complete (add --yes for automation)
```
@@ -1,27 +0,0 @@
## ADDED Requirements
### Requirement: Cline Tool Support
The system SHALL provide Cline (VS Code extension) as a supported tool option during OpenSpec initialization.
#### Scenario: Initialize project with Cline support
- **WHEN** user runs `openspec init --tools cline`
- **THEN** Cline-specific rule files are configured in `.clinerules/`
- **AND** CLINE.md root file includes OpenSpec workflow instructions
- **AND** Cline is registered as available configurator
#### Scenario: Cline proposal rule generation
- **WHEN** Cline rules are configured
- **THEN** `.clinerules/openspec-proposal.md` contains proposal workflow with guardrails
- **AND** Includes Cline-specific Markdown heading frontmatter
- **AND** Follows established slash command template pattern
#### Scenario: Cline apply and archive rules
- **WHEN** Cline rules are configured
- **THEN** `.clinerules/openspec-apply.md` contains implementation workflow
- **AND** `.clinerules/openspec-archive.md` contains archiving workflow
- **AND** Both commands include appropriate headers and references
#### Scenario: Cline root instructions
- **WHEN** Cline is selected during initialization
- **THEN** CLINE.md is created at project root
- **AND** Contains OpenSpec markers for managed content
- **AND** References `@/openspec/AGENTS.md` for workflow instructions
@@ -1,21 +0,0 @@
## ADDED Requirements
### Requirement: Crush Tool Support
The system SHALL provide Crush AI assistant as a supported tool option during OpenSpec initialization.
#### Scenario: Initialize project with Crush support
- **WHEN** user runs `openspec init --tool crush`
- **THEN** Crush-specific slash commands are configured in `.crush/commands/openspec/`
- **AND** Crush AGENTS.md includes OpenSpec workflow instructions
- **AND** Crush is registered as available configurator
#### Scenario: Crush proposal command generation
- **WHEN** Crush slash commands are configured
- **THEN** `.crush/commands/openspec/proposal.md` contains proposal workflow with guardrails
- **AND** Includes Crush-specific frontmatter with OpenSpec category and tags
- **AND** Follows established slash command template pattern
#### Scenario: Crush apply and archive commands
- **WHEN** Crush slash commands are configured
- **THEN** `.crush/commands/openspec/apply.md` contains implementation workflow
- **AND** `.crush/commands/openspec/archive.md` contains archiving workflow
- **AND** Both commands include appropriate frontmatter and references
@@ -11,8 +11,6 @@ The update command SHALL refresh existing slash command files for configured too
- **AND** ensure templates include instructions for the relevant workflow stage
- **AND** ensure the archive command includes `$ARGUMENTS` placeholder in frontmatter for accepting change ID arguments
## ADDED Requirements
### Requirement: Archive Command Argument Support
The archive slash command template SHALL support optional change ID arguments for tools that support `$ARGUMENTS` placeholder.
@@ -13,9 +13,3 @@
## 3. Update Documentation
- [x] 3.1 Update AGENTS.md archive examples to show argument usage
- [x] 3.2 Document that OpenCode now supports `/openspec:archive <change-id>`
## 4. Validation and Testing
- [ ] 4.1 Run `openspec update` to regenerate OpenCode slash commands
- [ ] 4.2 Manually test with OpenCode using `/openspec:archive <change-id>`
- [ ] 4.3 Test backward compatibility (archive command without arguments)
- [ ] 4.4 Run `openspec validate --strict` to ensure no issues
@@ -0,0 +1,97 @@
## MODIFIED Requirements
### Requirement: AI Tool Configuration Details
The command SHALL properly configure selected AI tools with OpenSpec-specific instructions using a marker system.
#### Scenario: Configuring Claude Code
- **WHEN** Claude Code is selected
- **THEN** create or update `CLAUDE.md` in the project root directory (not inside openspec/)
- **AND** populate the managed block with a short stub that points teammates to `@/openspec/AGENTS.md`
#### Scenario: Configuring CodeBuddy Code
- **WHEN** CodeBuddy Code is selected
- **THEN** create or update `CODEBUDDY.md` in the project root directory (not inside openspec/)
- **AND** populate the managed block with a short stub that points teammates to `@/openspec/AGENTS.md`
#### Scenario: Configuring Cline
- **WHEN** Cline is selected
- **THEN** create or update `CLINE.md` in the project root directory (not inside openspec/)
- **AND** populate the managed block with a short stub that points teammates to `@/openspec/AGENTS.md`
#### Scenario: Creating new CLAUDE.md
- **WHEN** CLAUDE.md does not exist
- **THEN** create new file with stub instructions wrapped in markers so the full workflow stays in `openspec/AGENTS.md`:
```markdown
<!-- OPENSPEC:START -->
# OpenSpec Instructions
This project uses OpenSpec to manage AI assistant workflows.
- Full guidance lives in '@/openspec/AGENTS.md'.
- Keep this managed block so 'openspec update' can refresh the instructions.
<!-- OPENSPEC:END -->
```
### 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 CodeBuddy Code
- **WHEN** the user selects CodeBuddy Code during initialization
- **THEN** create `.codebuddy/commands/openspec/proposal.md`, `.codebuddy/commands/openspec/apply.md`, and `.codebuddy/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 Cline
- **WHEN** the user selects Cline during initialization
- **THEN** create `.clinerules/openspec-proposal.md`, `.clinerules/openspec-apply.md`, and `.clinerules/openspec-archive.md`
- **AND** populate each file from shared templates so command text matches other tools
- **AND** include Cline-specific Markdown heading frontmatter
- **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
#### Scenario: Generating slash commands for OpenCode
- **WHEN** the user selects OpenCode during initialization
- **THEN** create `.opencode/commands/openspec-proposal.md`, `.opencode/commands/openspec-apply.md`, and `.opencode/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 Windsurf
- **WHEN** the user selects Windsurf during initialization
- **THEN** create `.windsurf/workflows/openspec-proposal.md`, `.windsurf/workflows/openspec-apply.md`, and `.windsurf/workflows/openspec-archive.md`
- **AND** populate each file from shared templates (wrapped in OpenSpec markers) so workflow text matches other tools
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
#### Scenario: Generating slash commands for Kilo Code
- **WHEN** the user selects Kilo Code during initialization
- **THEN** create `.kilocode/workflows/openspec-proposal.md`, `.kilocode/workflows/openspec-apply.md`, and `.kilocode/workflows/openspec-archive.md`
- **AND** populate each file from shared templates (wrapped in OpenSpec markers) so workflow text matches other tools
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
#### Scenario: Generating slash commands for Codex
- **WHEN** the user selects Codex during initialization
- **THEN** create global prompt files at `~/.codex/prompts/openspec-proposal.md`, `~/.codex/prompts/openspec-apply.md`, and `~/.codex/prompts/openspec-archive.md` (or under `$CODEX_HOME/prompts` if set)
- **AND** populate each file from shared templates that map the first numbered placeholder (`$1`) to the primary user input (e.g., change identifier or question text)
- **AND** wrap the generated content in OpenSpec markers so `openspec update` can refresh the prompts without touching surrounding custom notes
#### Scenario: Generating slash commands for GitHub Copilot
- **WHEN** the user selects GitHub Copilot during initialization
- **THEN** create `.github/prompts/openspec-proposal.prompt.md`, `.github/prompts/openspec-apply.prompt.md`, and `.github/prompts/openspec-archive.prompt.md`
- **AND** populate each file with YAML frontmatter containing a `description` field that summarizes the workflow stage
- **AND** include `$ARGUMENTS` placeholder to capture user input
- **AND** wrap the shared template body with OpenSpec markers so `openspec update` can refresh the content
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
@@ -0,0 +1,67 @@
## MODIFIED 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 CodeBuddy Code
- **WHEN** the user selects CodeBuddy Code during initialization
- **THEN** create `.codebuddy/commands/openspec/proposal.md`, `.codebuddy/commands/openspec/apply.md`, and `.codebuddy/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 Cline
- **WHEN** the user selects Cline during initialization
- **THEN** create `.clinerules/openspec-proposal.md`, `.clinerules/openspec-apply.md`, and `.clinerules/openspec-archive.md`
- **AND** populate each file from shared templates so command text matches other tools
- **AND** include Cline-specific Markdown heading frontmatter
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
#### Scenario: Generating slash commands for Crush
- **WHEN** the user selects Crush during initialization
- **THEN** create `.crush/commands/openspec/proposal.md`, `.crush/commands/openspec/apply.md`, and `.crush/commands/openspec/archive.md`
- **AND** populate each file from shared templates so command text matches other tools
- **AND** include Crush-specific frontmatter with OpenSpec category and tags
- **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
#### Scenario: Generating slash commands for OpenCode
- **WHEN** the user selects OpenCode during initialization
- **THEN** create `.opencode/commands/openspec-proposal.md`, `.opencode/commands/openspec-apply.md`, and `.opencode/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 Windsurf
- **WHEN** the user selects Windsurf during initialization
- **THEN** create `.windsurf/workflows/openspec-proposal.md`, `.windsurf/workflows/openspec-apply.md`, and `.windsurf/workflows/openspec-archive.md`
- **AND** populate each file from shared templates (wrapped in OpenSpec markers) so workflow text matches other tools
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
#### Scenario: Generating slash commands for Kilo Code
- **WHEN** the user selects Kilo Code during initialization
- **THEN** create `.kilocode/workflows/openspec-proposal.md`, `.kilocode/workflows/openspec-apply.md`, and `.kilocode/workflows/openspec-archive.md`
- **AND** populate each file from shared templates (wrapped in OpenSpec markers) so workflow text matches other tools
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
#### Scenario: Generating slash commands for Codex
- **WHEN** the user selects Codex during initialization
- **THEN** create global prompt files at `~/.codex/prompts/openspec-proposal.md`, `~/.codex/prompts/openspec-apply.md`, and `~/.codex/prompts/openspec-archive.md` (or under `$CODEX_HOME/prompts` if set)
- **AND** populate each file from shared templates that map the first numbered placeholder (`$1`) to the primary user input (e.g., change identifier or question text)
- **AND** wrap the generated content in OpenSpec markers so `openspec update` can refresh the prompts without touching surrounding custom notes
#### Scenario: Generating slash commands for GitHub Copilot
- **WHEN** the user selects GitHub Copilot during initialization
- **THEN** create `.github/prompts/openspec-proposal.prompt.md`, `.github/prompts/openspec-apply.prompt.md`, and `.github/prompts/openspec-archive.prompt.md`
- **AND** populate each file with YAML frontmatter containing a `description` field that summarizes the workflow stage
- **AND** include `$ARGUMENTS` placeholder to capture user input
- **AND** wrap the shared template body with OpenSpec markers so `openspec update` can refresh the content
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
+39
View File
@@ -64,6 +64,18 @@ The command SHALL properly configure selected AI tools with OpenSpec-specific in
- **THEN** create or update `CLAUDE.md` in the project root directory (not inside openspec/)
- **AND** populate the managed block with a short stub that points teammates to `@/openspec/AGENTS.md`
#### Scenario: Configuring CodeBuddy Code
- **WHEN** CodeBuddy Code is selected
- **THEN** create or update `CODEBUDDY.md` in the project root directory (not inside openspec/)
- **AND** populate the managed block with a short stub that points teammates to `@/openspec/AGENTS.md`
#### Scenario: Configuring Cline
- **WHEN** Cline is selected
- **THEN** create or update `CLINE.md` in the project root directory (not inside openspec/)
- **AND** populate the managed block with a short stub that points teammates to `@/openspec/AGENTS.md`
#### Scenario: Creating new CLAUDE.md
- **WHEN** CLAUDE.md does not exist
@@ -154,12 +166,39 @@ The init command SHALL generate slash command files for supported editors using
- **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 CodeBuddy Code
- **WHEN** the user selects CodeBuddy Code during initialization
- **THEN** create `.codebuddy/commands/openspec/proposal.md`, `.codebuddy/commands/openspec/apply.md`, and `.codebuddy/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 Cline
- **WHEN** the user selects Cline during initialization
- **THEN** create `.clinerules/openspec-proposal.md`, `.clinerules/openspec-apply.md`, and `.clinerules/openspec-archive.md`
- **AND** populate each file from shared templates so command text matches other tools
- **AND** include Cline-specific Markdown heading frontmatter
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
#### Scenario: Generating slash commands for Crush
- **WHEN** the user selects Crush during initialization
- **THEN** create `.crush/commands/openspec/proposal.md`, `.crush/commands/openspec/apply.md`, and `.crush/commands/openspec/archive.md`
- **AND** populate each file from shared templates so command text matches other tools
- **AND** include Crush-specific frontmatter with OpenSpec category and tags
- **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
#### Scenario: Generating slash commands for Factory Droid
- **WHEN** the user selects Factory Droid during initialization
- **THEN** create `.factory/commands/openspec-proposal.md`, `.factory/commands/openspec-apply.md`, and `.factory/commands/openspec-archive.md`
- **AND** populate each file from shared templates that include Factory-compatible YAML frontmatter for the `description` and `argument-hint` fields
- **AND** include the `$ARGUMENTS` placeholder in the template body so droid receives any user-supplied input
- **AND** wrap the generated content in OpenSpec managed markers so `openspec update` can safely refresh the commands
#### Scenario: Generating slash commands for OpenCode
- **WHEN** the user selects OpenCode during initialization
- **THEN** create `.opencode/commands/openspec-proposal.md`, `.opencode/commands/openspec-apply.md`, and `.opencode/commands/openspec-archive.md`
+46 -1
View File
@@ -50,22 +50,47 @@ The update command SHALL always update the core OpenSpec files and display an AS
- **AND** if a root-level stub exists, refresh it so it still directs contributors to `@/openspec/AGENTS.md`
### Requirement: Slash Command Updates
The update command SHALL refresh existing slash command files for configured tools without creating new ones.
The update command SHALL refresh existing slash command files for configured tools without creating new ones, and ensure the OpenCode archive command accepts change ID arguments.
#### 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 CodeBuddy Code
- **WHEN** `.codebuddy/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 Cline
- **WHEN** `.clinerules/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
- **THEN** refresh each file using shared templates
- **AND** include Cline-specific Markdown heading frontmatter
- **AND** ensure templates include instructions for the relevant workflow stage
#### Scenario: Updating slash commands for Crush
- **WHEN** `.crush/commands/` contains `openspec/proposal.md`, `openspec/apply.md`, and `openspec/archive.md`
- **THEN** refresh each file using shared templates
- **AND** include Crush-specific frontmatter with OpenSpec category and tags
- **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: Updating slash commands for Factory Droid
- **WHEN** `.factory/commands/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
- **THEN** refresh each file using the shared Factory templates that include YAML frontmatter for the `description` and `argument-hint` fields
- **AND** ensure the template body retains the `$ARGUMENTS` placeholder so user input keeps flowing into droid
- **AND** update only the content inside the OpenSpec managed markers, leaving any unmanaged notes untouched
- **AND** skip creating missing files during update
#### Scenario: Updating slash commands for OpenCode
- **WHEN** `.opencode/command/` 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
- **AND** ensure the archive command includes `$ARGUMENTS` placeholder in frontmatter for accepting change ID arguments
#### Scenario: Updating slash commands for Windsurf
- **WHEN** `.windsurf/workflows/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
@@ -96,6 +121,26 @@ The update command SHALL refresh existing slash command files for configured too
- **WHEN** a tool lacks a slash command file
- **THEN** do not create a new file during update
### Requirement: Archive Command Argument Support
The archive slash command template SHALL support optional change ID arguments for tools that support `$ARGUMENTS` placeholder.
#### Scenario: Archive command with change ID argument
- **WHEN** a user invokes `/openspec:archive <change-id>` with a change ID
- **THEN** the template SHALL instruct the AI to validate the provided change ID against `openspec list`
- **AND** use the provided change ID for archiving if valid
- **AND** fail fast if the provided change ID doesn't match an archivable change
#### Scenario: Archive command without argument (backward compatibility)
- **WHEN** a user invokes `/openspec:archive` without providing a change ID
- **THEN** the template SHALL instruct the AI to identify the change ID from context or by running `openspec list`
- **AND** proceed with the existing behavior (maintaining backward compatibility)
#### Scenario: OpenCode archive template generation
- **WHEN** generating the OpenCode archive slash command file
- **THEN** include the `$ARGUMENTS` placeholder in the frontmatter
- **AND** wrap it in a clear structure like `<ChangeId>\n $ARGUMENTS\n</ChangeId>` to indicate the expected argument
- **AND** include validation steps in the template body to check if the change ID is valid
## Edge Cases
### Requirement: Error Handling
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@fission-ai/openspec",
"version": "0.12.0",
"version": "0.13.0",
"description": "AI-native system for spec-driven development",
"keywords": [
"openspec",
+11 -11
View File
@@ -28,7 +28,7 @@ export class ChangeCommand {
*/
async show(changeName?: string, options?: { json?: boolean; requirementsOnly?: boolean; deltasOnly?: boolean; noInteractive?: boolean }): Promise<void> {
const changesPath = path.join(process.cwd(), 'openspec', 'changes');
if (!changeName) {
const canPrompt = isInteractive(options?.noInteractive);
const changes = await this.getActiveChanges(changesPath);
@@ -49,25 +49,25 @@ export class ChangeCommand {
return;
}
}
const proposalPath = path.join(changesPath, changeName, 'proposal.md');
try {
await fs.access(proposalPath);
} catch {
throw new Error(`Change "${changeName}" not found at ${proposalPath}`);
}
if (options?.json) {
const jsonOutput = await this.converter.convertChangeToJson(proposalPath);
if (options.requirementsOnly) {
console.error('Flag --requirements-only is deprecated; use --deltas-only instead.');
}
const parsed: Change = JSON.parse(jsonOutput);
const contentForTitle = await fs.readFile(proposalPath, 'utf-8');
const title = this.extractTitle(contentForTitle);
const title = this.extractTitle(contentForTitle, changeName);
const id = parsed.name;
const deltas = parsed.deltas || [];
@@ -124,7 +124,7 @@ export class ChangeCommand {
return {
id: changeName,
title: this.extractTitle(content),
title: this.extractTitle(content, changeName),
deltaCount: change.deltas.length,
taskStatus,
};
@@ -159,7 +159,7 @@ export class ChangeCommand {
const tasksPath = path.join(changesPath, changeName, 'tasks.md');
try {
const content = await fs.readFile(proposalPath, 'utf-8');
const title = this.extractTitle(content);
const title = this.extractTitle(content, changeName);
let taskStatusText = '';
try {
const tasksContent = await fs.readFile(tasksPath, 'utf-8');
@@ -258,9 +258,9 @@ export class ChangeCommand {
}
}
private extractTitle(content: string): string {
const match = content.match(/^#\s+(?:Change:\s+)?(.+)$/m);
return match ? match[1].trim() : 'Untitled Change';
private extractTitle(content: string, changeName: string): string {
const match = content.match(/^#\s+(?:Change:\s+)?(.+)$/im);
return match ? match[1].trim() : changeName;
}
private countTasks(content: string): { total: number; completed: number } {
+1 -1
View File
@@ -20,7 +20,7 @@ export const AI_TOOLS: AIToolOption[] = [
{ name: 'Auggie (Augment CLI)', value: 'auggie', available: true, successLabel: 'Auggie' },
{ name: 'Claude Code', value: 'claude', available: true, successLabel: 'Claude Code' },
{ name: 'Cline', value: 'cline', available: true, successLabel: 'Cline' },
{ name: 'CodeBuddy', value: 'codebuddy', available: true, successLabel: 'CodeBuddy' },
{ name: 'CodeBuddy Code (CLI)', value: 'codebuddy', available: true, successLabel: 'CodeBuddy Code' },
{ name: 'Crush', value: 'crush', available: true, successLabel: 'Crush' },
{ name: 'Cursor', value: 'cursor', available: true, successLabel: 'Cursor' },
{ name: 'Factory Droid', value: 'factory', available: true, successLabel: 'Factory Droid' },
+1 -1
View File
@@ -6,7 +6,7 @@ import { OPENSPEC_MARKERS } from '../config.js';
export class CodeBuddyConfigurator implements ToolConfigurator {
name = 'CodeBuddy';
configFileName = 'CodeBuddy.md';
configFileName = 'CODEBUDDY.md';
isAvailable = true;
async configure(projectPath: string, openspecDir: string): Promise<void> {
+64 -20
View File
@@ -21,6 +21,7 @@ import {
AI_TOOLS,
OPENSPEC_DIR_NAME,
AIToolOption,
OPENSPEC_MARKERS,
} from './config.js';
import { PALETTE } from './styles/palette.js';
@@ -388,7 +389,7 @@ export class InitCommand {
// Validation happens silently in the background
const extendMode = await this.validate(projectPath, openspecPath);
const existingToolStates = await this.getExistingToolStates(projectPath);
const existingToolStates = await this.getExistingToolStates(projectPath, extendMode);
this.renderBanner(extendMode);
@@ -627,35 +628,78 @@ export class InitCommand {
}
private async getExistingToolStates(
projectPath: string
projectPath: string,
extendMode: boolean
): Promise<Record<string, boolean>> {
const states: Record<string, boolean> = {};
for (const tool of AI_TOOLS) {
states[tool.value] = await this.isToolConfigured(projectPath, tool.value);
// Fresh initialization - no tools configured yet
if (!extendMode) {
return Object.fromEntries(AI_TOOLS.map(t => [t.value, false]));
}
return states;
// Extend mode - check all tools in parallel for better performance
const entries = await Promise.all(
AI_TOOLS.map(async (t) => [t.value, await this.isToolConfigured(projectPath, t.value)] as const)
);
return Object.fromEntries(entries);
}
private async isToolConfigured(
projectPath: string,
toolId: string
): Promise<boolean> {
const configFile = ToolRegistry.get(toolId)?.configFileName;
if (
configFile &&
(await FileSystemUtils.fileExists(path.join(projectPath, configFile)))
)
return true;
// A tool is only considered "configured by OpenSpec" if its files contain OpenSpec markers.
// For tools with both config files and slash commands, BOTH must have markers.
// For slash commands, at least one file with markers is sufficient (not all required).
const slashConfigurator = SlashCommandRegistry.get(toolId);
if (!slashConfigurator) return false;
for (const target of slashConfigurator.getTargets()) {
const absolute = slashConfigurator.resolveAbsolutePath(
projectPath,
target.id
);
if (await FileSystemUtils.fileExists(absolute)) return true;
// Helper to check if a file exists and contains OpenSpec markers
const fileHasMarkers = async (absolutePath: string): Promise<boolean> => {
try {
const content = await FileSystemUtils.readFile(absolutePath);
return content.includes(OPENSPEC_MARKERS.start) && content.includes(OPENSPEC_MARKERS.end);
} catch {
return false;
}
};
let hasConfigFile = false;
let hasSlashCommands = false;
// Check if the tool has a config file with OpenSpec markers
const configFile = ToolRegistry.get(toolId)?.configFileName;
if (configFile) {
const configPath = path.join(projectPath, configFile);
hasConfigFile = (await FileSystemUtils.fileExists(configPath)) && (await fileHasMarkers(configPath));
}
// Check if any slash command file exists with OpenSpec markers
const slashConfigurator = SlashCommandRegistry.get(toolId);
if (slashConfigurator) {
for (const target of slashConfigurator.getTargets()) {
const absolute = slashConfigurator.resolveAbsolutePath(projectPath, target.id);
if ((await FileSystemUtils.fileExists(absolute)) && (await fileHasMarkers(absolute))) {
hasSlashCommands = true;
break; // At least one file with markers is sufficient
}
}
}
// Tool is only configured if BOTH exist with markers
// OR if the tool has no config file requirement (slash commands only)
// OR if the tool has no slash commands requirement (config file only)
const hasConfigFileRequirement = configFile !== undefined;
const hasSlashCommandRequirement = slashConfigurator !== undefined;
if (hasConfigFileRequirement && hasSlashCommandRequirement) {
// Both are required - both must be present with markers
return hasConfigFile && hasSlashCommands;
} else if (hasConfigFileRequirement) {
// Only config file required
return hasConfigFile;
} else if (hasSlashCommandRequirement) {
// Only slash commands required
return hasSlashCommands;
}
return false;
}
+2 -2
View File
@@ -95,7 +95,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-id> [--yes|-y] # Archive after deployment (add --yes for non-interactive runs)
@@ -161,6 +160,8 @@ New request?
2. **Write proposal.md:**
\`\`\`markdown
# Change: [Brief description of change]
## Why
[1-2 sentences on problem/opportunity]
@@ -448,7 +449,6 @@ 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-id> [--yes|-y] # Mark complete (add --yes for automation)
\`\`\`
+114
View File
@@ -865,6 +865,39 @@ describe('InitCommand', () => {
expect(codeBuddyChoice.configured).toBe(true);
});
it('should create CODEBUDDY.md when CodeBuddy is selected', async () => {
queueSelections('codebuddy', DONE);
await initCommand.execute(testDir);
const codeBuddyPath = path.join(testDir, 'CODEBUDDY.md');
expect(await fileExists(codeBuddyPath)).toBe(true);
const content = await fs.readFile(codeBuddyPath, 'utf-8');
expect(content).toContain('<!-- OPENSPEC:START -->');
expect(content).toContain("@/openspec/AGENTS.md");
expect(content).toContain('openspec update');
expect(content).toContain('<!-- OPENSPEC:END -->');
});
it('should update existing CODEBUDDY.md with markers', async () => {
queueSelections('codebuddy', DONE);
const codeBuddyPath = path.join(testDir, 'CODEBUDDY.md');
const existingContent =
'# My CodeBuddy Instructions\nCustom instructions here';
await fs.writeFile(codeBuddyPath, existingContent);
await initCommand.execute(testDir);
const updatedContent = await fs.readFile(codeBuddyPath, 'utf-8');
expect(updatedContent).toContain('<!-- OPENSPEC:START -->');
expect(updatedContent).toContain("@/openspec/AGENTS.md");
expect(updatedContent).toContain('openspec update');
expect(updatedContent).toContain('<!-- OPENSPEC:END -->');
expect(updatedContent).toContain('Custom instructions here');
});
it('should create Crush slash command files with templates', async () => {
queueSelections('crush', DONE);
@@ -1018,6 +1051,87 @@ describe('InitCommand', () => {
});
});
describe('already configured detection', () => {
it('should NOT show tools as already configured in fresh project with existing CLAUDE.md', async () => {
// Simulate user having their own CLAUDE.md before running openspec init
const claudePath = path.join(testDir, 'CLAUDE.md');
await fs.writeFile(claudePath, '# My Custom Claude Instructions\n');
queueSelections('claude', DONE);
await initCommand.execute(testDir);
// In the first run (non-interactive mode via queueSelections),
// the prompt is called with configured: false for claude
const firstCallArgs = mockPrompt.mock.calls[0][0];
const claudeChoice = firstCallArgs.choices.find(
(choice: any) => choice.value === 'claude'
);
expect(claudeChoice.configured).toBe(false);
});
it('should NOT show tools as already configured in fresh project with existing slash commands', async () => {
// Simulate user having their own custom slash commands
const customCommandDir = path.join(testDir, '.claude/commands/custom');
await fs.mkdir(customCommandDir, { recursive: true });
await fs.writeFile(
path.join(customCommandDir, 'mycommand.md'),
'# My Custom Command\n'
);
queueSelections('claude', DONE);
await initCommand.execute(testDir);
const firstCallArgs = mockPrompt.mock.calls[0][0];
const claudeChoice = firstCallArgs.choices.find(
(choice: any) => choice.value === 'claude'
);
expect(claudeChoice.configured).toBe(false);
});
it('should show tools as already configured in extend mode', async () => {
// First initialization
queueSelections('claude', DONE);
await initCommand.execute(testDir);
// Second initialization (extend mode)
queueSelections('cursor', DONE);
await initCommand.execute(testDir);
const secondCallArgs = mockPrompt.mock.calls[1][0];
const claudeChoice = secondCallArgs.choices.find(
(choice: any) => choice.value === 'claude'
);
expect(claudeChoice.configured).toBe(true);
});
it('should NOT show already configured for Codex in fresh init even with global prompts', async () => {
// Create global Codex prompts (simulating previous installation)
const codexPromptsDir = path.join(testDir, '.codex/prompts');
await fs.mkdir(codexPromptsDir, { recursive: true });
await fs.writeFile(
path.join(codexPromptsDir, 'openspec-proposal.md'),
'# Existing prompt\n'
);
queueSelections('claude', DONE);
await initCommand.execute(testDir);
const firstCallArgs = mockPrompt.mock.calls[0][0];
const codexChoice = firstCallArgs.choices.find(
(choice: any) => choice.value === 'codex'
);
// In fresh init, even global tools should not show as configured
expect(codexChoice.configured).toBe(false);
});
});
describe('error handling', () => {
it('should provide helpful error for insufficient permissions', async () => {
// This is tricky to test cross-platform, but we can test the error message