Compare commits

..
Author SHA1 Message Date
Tabish Bidiwale 7b0f494754 feat(cli-init): propose additional agent init flow 2025-09-17 08:33:10 +10:00
Tabish Bidiwale 21a0e74b74 Merge pull request #66 from Fission-AI/changeset-release/main
chore(release): version packages
2025-09-16 16:50:42 +10:00
github-actions[bot] 485ef07ec7 Version Packages 2025-09-16 06:49:43 +00:00
Tabish Bidiwale ce5ceadbe7 chore(release): add changeset for dashboard release 2025-09-16 16:49:21 +10:00
Tabish Bidiwale 9a173b917c docs: refresh readme hero 2025-09-16 16:38:22 +10:00
Tabish Bidiwale 79baabbed1 Merge pull request #65 from Fission-AI/update-slash-commands
Update slash command guardrails
2025-09-16 15:48:02 +10:00
Tabish Bidiwale 818a5922ce docs: reference agents conventions in slash guardrails 2025-09-16 15:46:53 +10:00
Tabish Bidiwale d8d2930182 docs(templates): update slash command instructions 2025-09-16 15:05:59 +10:00
Tabish Bidiwale 6af6e0ccb6 Merge pull request #62 from Fission-AI/codex/implement-update-agent-file-name-change
feat: rename agent instructions file to AGENTS.md
2025-09-16 13:32:30 +10:00
Tabish Bidiwale 50e6660018 test: update update command logs 2025-09-16 13:28:36 +10:00
Tabish Bidiwale 4bbb52dda4 feat: rename agent instructions file 2025-09-16 13:17:49 +10:00
Tabish Bidiwale 4a0ae49b6e Merge pull request #64 from Fission-AI/codex/add-sorting-for-active-changes-by-completion
feat: add active change sorting proposal
2025-09-16 12:00:33 +10:00
Tabish Bidiwale 5b2049aedf feat(view): sort active changes by progress 2025-09-16 11:59:40 +10:00
Tabish Bidiwale 332020ac6b fix: clarify active change sorting tasks 2025-09-16 11:49:57 +10:00
Tabish Bidiwale 646c516b0d Merge pull request #63 from Fission-AI/feat/add-slash-command-support
feat(cli): add slash command support
2025-09-16 11:49:22 +10:00
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
43 changed files with 1134 additions and 68 deletions
+8
View File
@@ -1,5 +1,13 @@
# @fission-ai/openspec
## 0.2.0
### Minor Changes
- ce5cead: - Add an `openspec view` dashboard that rolls up spec counts and change progress at a glance
- Generate and update AI slash commands alongside the renamed `openspec/AGENTS.md` instructions file
- Remove the deprecated `openspec diff` command and direct users to `openspec show`
## 0.1.0
### Minor Changes
+16 -15
View File
@@ -17,9 +17,17 @@
<a href="https://conventionalcommits.org"><img alt="Conventional Commits" src="https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg?style=flat-square" /></a>
</p>
<p align="center">
<img src="assets/openspec_dashboard.png" alt="OpenSpec dashboard preview" width="90%">
</p>
<p align="center">
Follow <a href="https://x.com/0xTab">@0xTab on X</a> for updates.
</p>
# OpenSpec
**Supported AI Tools:** ✅ Claude Code | 🔜 Cursor (coming soon) | 🔜 AGENTS.md support (coming soon)
**Supported AI Tools:** ✅ Claude Code | 🔜 Cursor (coming soon) | ✅ AGENTS.md instructions
Create **alignment** between humans and AI coding assistants through spec-driven development. **No API keys required.**
@@ -31,20 +39,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
@@ -99,7 +100,7 @@ openspec init
# openspec/
# ├── specs/ # Current specifications (truth)
# ├── changes/ # Proposed changes
# └── README.md # AI instructions for your tool
# └── AGENTS.md # AI instructions for your tool
```
### 2. Create Your First Change
Binary file not shown.

After

Width:  |  Height:  |  Size: 450 KiB

+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,35 @@
# Allow Additional AI Tool Initialization After Setup
## Summary
- Let `openspec init` configure new AI coding tools for projects that already contain an OpenSpec structure.
- Keep the initialization flow safe by skipping structure creation and only generating files for tools the user explicitly selects.
- Provide clear feedback so users know which tool files were added versus already present.
## Motivation
Today `openspec init` exits with an error once an `openspec/` directory exists. That protects the directory layout, but it blocks
teams that start with one assistant (for example, Claude Code) and later want to add another such as Cursor. They have to create
those files by hand or rerun `init` in a clean clone, which undermines the "easy onboarding" promise. Letting the command extend
an existing installation keeps the workflow consistent and avoids manual file management.
## Proposal
1. Detect an existing OpenSpec structure at the start of `openspec init` and branch into an "extend" mode instead of exiting.
- Announce that the base structure already exists and that the command will only manage AI tool configuration files.
- Keep the existing guard for directories or files we must not overwrite.
2. Present the usual AI tool selection prompt even in extend mode, showing which tools are already configured.
- Skip disabled options that remain "coming soon".
- Mark already configured tools as such so users know whether selecting them will refresh or add files.
3. When the user selects additional tools, generate the same initialization files that a fresh run would create (e.g., Cursor
workspace files) while leaving untouched tools intact apart from marker-managed sections.
- Do nothing when the user selects no new tools and keep the previous error messaging to avoid silently succeeding.
4. Summarize the outcome (created, refreshed, skipped) before exiting with code 0 when work was performed.
- Include friendly guidance that future updates to shared content still come from `openspec update`.
## Out of Scope
- Changing how `openspec update` discovers or updates AI tool files.
- Supporting brand-new AI tools beyond those already wired into the CLI.
- Adding non-interactive flags for selecting multiple tools in one run (follow-up if needed).
## Risks & Mitigations
- **User confusion about extend mode** → Explicitly log what will happen before prompting and summarise results afterward.
- **Accidental overwrites** → Continue using marker-based updates and skip files unless the user chooses that tool.
- **Inconsistent state if init fails mid-run** → Reuse existing rollback/transaction logic so partial writes clean up.
@@ -0,0 +1,20 @@
## MODIFIED Requirements
### Requirement: Safety Checks
The command SHALL perform safety checks to prevent overwriting existing structures and ensure proper permissions.
#### Scenario: Detecting existing initialization
- **WHEN** the `openspec/` directory already exists
- **THEN** inform the user that OpenSpec is already initialized and skip recreating the base structure
- **AND** continue to the AI tool selection step so additional tools can be configured
- **AND** display the existing-initialization error message only when the user declines to add any AI tools
## ADDED Requirements
### Requirement: Additional AI Tool Initialization
`openspec init` SHALL allow users to add configuration files for new AI coding assistants after the initial setup.
#### Scenario: Configuring an extra tool after initial setup
- **GIVEN** an `openspec/` directory already exists and at least one AI tool file is present
- **WHEN** the user runs `openspec init` and selects a different supported AI tool
- **THEN** generate that tool's configuration files with OpenSpec markers the same way as during first-time initialization
- **AND** leave existing tool configuration files unchanged except for managed sections that need refreshing
- **AND** exit with code 0 and display a success summary highlighting the newly added tool files
@@ -0,0 +1,16 @@
# Implementation Tasks
## 1. Extend Init Guard
- [ ] 1.1 Detect existing OpenSpec structures at the start of `openspec init` and enter an extend mode instead of failing.
- [ ] 1.2 Log that core scaffolding will be skipped while still protecting against missing write permissions.
## 2. Update AI Tool Selection
- [ ] 2.1 Present AI tool choices even in extend mode, indicating which tools are already configured.
- [ ] 2.2 Ensure disabled "coming soon" tools remain non-selectable.
## 3. Generate Additional Tool Files
- [ ] 3.1 Create configuration files for newly selected tools while leaving untouched tools unaffected apart from marker-managed sections.
- [ ] 3.2 Summarize created, refreshed, and skipped tools before exiting with the appropriate code.
## 4. Verification
- [ ] 4.1 Add tests covering rerunning `openspec init` to add another tool and the scenario where the user declines to add anything.
@@ -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,25 @@
# Change: Sort Active Changes by Progress
## Problem
- The dashboard currently lists active changes in filesystem discovery order.
- Users cannot quickly spot proposals that have not started or are nearly complete.
- Inconsistent ordering between runs makes it harder to track progress when many changes exist.
## Proposal
1. Update the Active Changes list in the dashboard to sort by percentage of completion in ascending order so 0% items show first.
2. When two changes share the same completion percentage, break ties deterministically by change identifier (alphabetical).
## Benefits
- Highlights work that has not started yet, enabling quicker prioritization.
- Provides consistent ordering across machines and repeated runs.
- Keeps the dashboard compact while communicating the most important status signal.
## Risks & Mitigations
- **Risk:** Sorting logic could regress rendering when progress data is missing.
- **Mitigation:** Treat missing progress as 0% so items still surface and document behavior in tests.
- **Risk:** Additional sorting could impact performance for large change sets.
- **Mitigation:** The number of active changes is typically small; sorting a few entries is negligible.
## Success Criteria
- Dashboard output shows active changes ordered by ascending completion percentage with deterministic tie-breaking.
- Unit coverage verifying the sort when percentages vary and when ties occur.
@@ -0,0 +1,9 @@
## MODIFIED Requirements
### Requirement: Active Changes Display
The dashboard SHALL show active changes with visual progress indicators.
#### Scenario: Active changes ordered by completion percentage
- **WHEN** multiple active changes are displayed with progress information
- **THEN** list them sorted by completion percentage ascending so 0% items appear first
- **AND** treat missing progress values as 0% for ordering
- **AND** break ties by change identifier in ascending alphabetical order to keep output deterministic
@@ -0,0 +1,8 @@
# Implementation Tasks
## 1. Dashboard Sorting Logic
- [x] 1.1 Update the Active Changes rendering to sort by completion percentage ascending.
- [x] 1.2 Treat missing progress as 0% and break ties alphabetically by change identifier.
## 2. Verification
- [x] 2.1 Add tests that cover different completion percentages and tie cases to confirm deterministic ordering.
@@ -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
- [x] Rename `openspec/README.md` to `openspec/AGENTS.md`
- [x] Update root references to new path
## 2. Update Templates
- [x] Rename `src/core/templates/readme-template.ts` to `agents-template.ts`
- [x] Update exported constant from `readmeTemplate` to `agentsTemplate`
## 3. Adjust CLI Commands
- [x] Modify `openspec init` to generate `AGENTS.md`
- [x] Update `openspec update` to refresh `AGENTS.md`
- [x] Ensure CLAUDE.md markers link to `@openspec/AGENTS.md`
## 4. Update Specifications
- [x] Modify `cli-init` spec to reference `AGENTS.md`
- [x] Modify `cli-update` spec to reference `AGENTS.md`
- [x] Modify `openspec-conventions` spec to include `AGENTS.md` in project structure
## 5. Validation
- [x] `pnpm test`
+4 -4
View File
@@ -31,7 +31,7 @@ The command SHALL create the complete OpenSpec directory structure with all requ
```
openspec/
├── project.md
├── README.md
├── AGENTS.md
├── specs/
└── changes/
└── archive/
@@ -44,7 +44,7 @@ The command SHALL generate required template files with appropriate content for
#### Scenario: Generating template files
- **WHEN** initializing OpenSpec
- **THEN** generate `README.md` containing complete OpenSpec instructions for AI assistants
- **THEN** generate `AGENTS.md` containing complete OpenSpec instructions for AI assistants
- **AND** generate `project.md` with project context template
### Requirement: AI Tool Configuration
@@ -80,7 +80,7 @@ This document provides instructions for AI coding assistants on how to use OpenS
This project uses OpenSpec for spec-driven development. Specifications are the source of truth.
See @openspec/README.md for detailed conventions and guidelines.
See @openspec/AGENTS.md for detailed conventions and guidelines.
<!-- OPENSPEC:END -->
```
@@ -164,7 +164,7 @@ Next steps - Copy these prompts to Claude:
OpenSpec change proposal for this feature"
3. Learn the OpenSpec workflow:
"Please explain the OpenSpec workflow from openspec/README.md
"Please explain the OpenSpec workflow from openspec/AGENTS.md
and how I should work with you on this project"
────────────────────────────────────────────────────────────
```
+3 -3
View File
@@ -13,7 +13,7 @@ The update command SHALL update OpenSpec instruction files to the latest templat
- **WHEN** a user runs `openspec update`
- **THEN** the command SHALL:
- Check if the `openspec` directory exists
- Replace `openspec/README.md` with the latest template (complete replacement)
- Replace `openspec/AGENTS.md` with the latest template (complete replacement)
- Update **only existing** AI tool configuration files (e.g., CLAUDE.md)
- Check each registered AI tool configurator
- For each configurator, check if its file exists
@@ -40,7 +40,7 @@ The update command SHALL handle file updates in a predictable and safe manner.
#### Scenario: Updating files
- **WHEN** updating files
- **THEN** completely replace `openspec/README.md` with the latest template
- **THEN** completely replace `openspec/AGENTS.md` with the latest template
- **AND** update only the OpenSpec-managed blocks in **existing** AI tool files using markers
- **AND** use the default directory name `openspec`
- **AND** be idempotent (repeated runs have no additional effect)
@@ -64,7 +64,7 @@ The update command SHALL always update the core OpenSpec files and display an AS
#### Scenario: Successful update
- **WHEN** the update completes successfully
- **THEN** replace `openspec/README.md` with the latest template
- **THEN** replace `openspec/AGENTS.md` with the latest template
- **AND** update existing AI tool configuration files within markers
- **AND** display the message: "Updated OpenSpec instructions"
+7
View File
@@ -46,6 +46,13 @@ The dashboard SHALL show active changes with visual progress indicators.
- **AND** visual progress bar using Unicode characters
- **AND** percentage completion on the right
#### Scenario: Active changes ordered by completion percentage
- **WHEN** multiple active changes are displayed with progress information
- **THEN** list them sorted by completion percentage ascending so 0% items appear first
- **AND** treat missing progress values as 0% for ordering
- **AND** break ties by change identifier in ascending alphabetical order to keep output deterministic
#### Scenario: No active changes
- **WHEN** all changes are completed or no changes exist
+2 -2
View File
@@ -24,7 +24,7 @@ An OpenSpec project SHALL maintain a consistent directory structure for specific
```
openspec/
├── project.md # Project-specific context
├── README.md # AI assistant instructions
├── AGENTS.md # AI assistant instructions
├── specs/ # Current deployed capabilities
│ └── [capability]/ # Single, focused capability
│ ├── spec.md # WHAT and WHY
@@ -245,7 +245,7 @@ An OpenSpec project SHALL maintain a consistent directory structure for specific
```
openspec/
├── project.md # Project-specific context
├── README.md # AI assistant instructions
├── AGENTS.md # AI assistant instructions
├── specs/ # Current deployed capabilities
│ └── [capability]/ # Single, focused capability
│ ├── spec.md # WHAT and WHY
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@fission-ai/openspec",
"version": "0.1.0",
"version": "0.2.0",
"description": "AI-native system for spec-driven development",
"keywords": [
"openspec",
+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());
}
}
+8 -2
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);
}
}
}
@@ -126,8 +132,8 @@ export class InitCommand {
console.log(' "I want to add [YOUR FEATURE HERE]. Please create an');
console.log(' OpenSpec change proposal for this feature"\n');
console.log('3. Learn the OpenSpec workflow:');
console.log(' "Please explain the OpenSpec workflow from openspec/README.md');
console.log(' "Please explain the OpenSpec workflow from openspec/AGENTS.md');
console.log(' and how I should work with you on this project"');
console.log('────────────────────────────────────────────────────────────\n');
}
}
}
@@ -1,4 +1,4 @@
export const readmeTemplate = `# OpenSpec Instructions
export const agentsTemplate = `# OpenSpec Instructions
Instructions for AI coding assistants using OpenSpec for spec-driven development.
@@ -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
+1 -1
View File
@@ -2,7 +2,7 @@ export const claudeTemplate = `# OpenSpec Project
This project uses OpenSpec for spec-driven development. Specifications are the source of truth.
See @openspec/README.md for detailed conventions and guidelines.
See @openspec/AGENTS.md for detailed conventions and guidelines.
## Three-Stage Workflow
+10 -4
View File
@@ -1,6 +1,7 @@
import { readmeTemplate } from './readme-template.js';
import { agentsTemplate } from './agents-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;
@@ -11,8 +12,8 @@ export class TemplateManager {
static getTemplates(context: ProjectContext = {}): Template[] {
return [
{
path: 'README.md',
content: readmeTemplate
path: 'AGENTS.md',
content: agentsTemplate
},
{
path: 'project.md',
@@ -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';
@@ -0,0 +1,50 @@
export type SlashCommandId = 'proposal' | 'apply' | 'archive';
const baseGuardrails = `**Guardrails**
- Favor straightforward, minimal implementations first and add complexity only when it is requested or clearly required.
- Keep changes tightly scoped to the requested outcome.
- Refer to \`openspec/AGENTS.md\` if you need additional OpenSpec conventions or clarifications.`;
const proposalGuardrails = `${baseGuardrails}\n- Identify any vague or ambiguous details and ask the necessary follow-up questions before editing files.`;
const proposalSteps = `**Steps**
1. Review \`openspec/project.md\`, run \`openspec list\` and \`openspec list --specs\`, and inspect related code or docs (e.g., via \`rg\`/\`ls\`) to ground the proposal in current behaviour; note any gaps that require clarification.
2. Choose a unique verb-led \`change-id\` and scaffold \`proposal.md\`, \`tasks.md\`, and \`design.md\` (when needed) under \`openspec/changes/<id>/\`.
3. Map the change into concrete capabilities or requirements, breaking multi-scope efforts into distinct spec deltas with clear relationships and sequencing.
4. Capture architectural reasoning in \`design.md\` when the solution spans multiple systems, introduces new patterns, or demands trade-off discussion before committing to specs.
5. Draft spec deltas in \`changes/<id>/specs/\` using \`## ADDED|MODIFIED|REMOVED Requirements\` with at least one \`#### Scenario:\` per requirement and cross-reference related capabilities when relevant.
6. Draft \`tasks.md\` as an ordered list of small, verifiable work items that deliver user-visible progress, include validation (tests, tooling), and highlight dependencies or parallelizable work.
7. 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.
- Explore the codebase with \`rg <keyword>\`, \`ls\`, or direct file reads so proposals align with current implementation realities.`;
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. Identify the requested change ID (via the prompt or \`openspec list\`).
2. Run \`openspec archive <id>\` to let the CLI move the change and apply spec updates (use \`--skip-specs\` only for tooling-only work).
3. Review the command output to confirm the target specs were updated and the change landed in \`changes/archive/\`.
4. Validate with \`openspec validate --strict\` and inspect with \`openspec show <id>\` if anything looks off.`;
const archiveReferences = `**Reference**
- Inspect refreshed specs with \`openspec list --specs\` and address any validation issues before handing off.`;
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];
}
+36 -5
View File
@@ -1,8 +1,9 @@
import path from 'path';
import { FileSystemUtils } from '../utils/file-system.js';
import { OPENSPEC_DIR_NAME } from './config.js';
import { readmeTemplate } from './templates/readme-template.js';
import { agentsTemplate } from './templates/agents-template.js';
import { ToolRegistry } from './configurators/registry.js';
import { SlashCommandRegistry } from './configurators/slash/registry.js';
export class UpdateCommand {
async execute(projectPath: string): Promise<void> {
@@ -15,14 +16,17 @@ export class UpdateCommand {
throw new Error(`No OpenSpec directory found. Run 'openspec init' first.`);
}
// 2. Update README.md (full replacement)
const readmePath = path.join(openspecPath, 'README.md');
await FileSystemUtils.writeFile(readmePath, readmeTemplate);
// 2. Update AGENTS.md (full replacement)
const agentsPath = path.join(openspecPath, 'AGENTS.md');
await FileSystemUtils.writeFile(agentsPath, agentsTemplate);
// 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);
@@ -30,6 +34,9 @@ export class UpdateCommand {
// Only update if the file already exists
if (await FileSystemUtils.fileExists(configFilePath)) {
try {
if (!await FileSystemUtils.canWriteFile(configFilePath)) {
throw new Error(`Insufficient permissions to modify ${configurator.configFileName}`);
}
await configurator.configure(resolvedProjectPath, openspecPath);
updatedFiles.push(configurator.configFileName);
} catch (error) {
@@ -39,16 +46,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)'];
const messages: string[] = ['Updated OpenSpec instructions (AGENTS.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'));
}
+9 -2
View File
@@ -95,8 +95,15 @@ export class ViewCommand {
}
}
// Sort alphabetically
active.sort((a, b) => a.name.localeCompare(b.name));
// Sort active changes by completion percentage (ascending) and then by name for deterministic ordering
active.sort((a, b) => {
const percentageA = a.progress.total > 0 ? a.progress.completed / a.progress.total : 0;
const percentageB = b.progress.total > 0 ? b.progress.completed / b.progress.total : 0;
if (percentageA < percentageB) return -1;
if (percentageA > percentageB) return 1;
return a.name.localeCompare(b.name);
});
completed.sort((a, b) => a.name.localeCompare(b.name));
return { active, completed };
+19
View File
@@ -18,6 +18,25 @@ export class FileSystemUtils {
}
}
static async canWriteFile(filePath: string): Promise<boolean> {
try {
const stats = await fs.stat(filePath);
if (!stats.isFile()) {
return true;
}
return (stats.mode & 0o222) !== 0;
} catch (error: any) {
if (error.code === 'ENOENT') {
return true;
}
console.debug(`Unable to determine write permissions for ${filePath}: ${error.message}`);
return false;
}
}
static async directoryExists(dirPath: string): Promise<boolean> {
try {
const stats = await fs.stat(dirPath);
+62 -8
View File
@@ -40,17 +40,17 @@ describe('InitCommand', () => {
expect(await directoryExists(path.join(openspecPath, 'changes', 'archive'))).toBe(true);
});
it('should create README.md and project.md', async () => {
it('should create AGENTS.md and project.md', async () => {
vi.mocked(prompts.select).mockResolvedValue('claude');
await initCommand.execute(testDir);
const openspecPath = path.join(testDir, 'openspec');
expect(await fileExists(path.join(openspecPath, 'README.md'))).toBe(true);
expect(await fileExists(path.join(openspecPath, 'AGENTS.md'))).toBe(true);
expect(await fileExists(path.join(openspecPath, 'project.md'))).toBe(true);
const readmeContent = await fs.readFile(path.join(openspecPath, 'README.md'), 'utf-8');
expect(readmeContent).toContain('OpenSpec Instructions');
const agentsContent = await fs.readFile(path.join(openspecPath, 'AGENTS.md'), 'utf-8');
expect(agentsContent).toContain('OpenSpec Instructions');
const projectContent = await fs.readFile(path.join(openspecPath, 'project.md'), 'utf-8');
expect(projectContent).toContain('Project Context');
@@ -86,6 +86,60 @@ 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>');
expect(archiveContent).toContain('`--skip-specs` only for tooling-only work');
});
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 +234,4 @@ async function directoryExists(dirPath: string): Promise<boolean> {
} catch {
return false;
}
}
}
+107 -14
View File
@@ -56,11 +56,42 @@ More content after.`;
// Check console output
expect(consoleSpy).toHaveBeenCalledWith(
'Updated OpenSpec instructions (README.md)\nUpdated AI tool files: CLAUDE.md'
'Updated OpenSpec instructions (AGENTS.md)\nUpdated AI tool files: CLAUDE.md'
);
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 (AGENTS.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,13 +104,43 @@ 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 (AGENTS.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');
await updateCommand.execute(testDir);
// Should only update OpenSpec instructions
expect(consoleSpy).toHaveBeenCalledWith('Updated OpenSpec instructions (README.md)');
expect(consoleSpy).toHaveBeenCalledWith('Updated OpenSpec instructions (AGENTS.md)');
consoleSpy.mockRestore();
});
@@ -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');
@@ -96,11 +158,33 @@ More content after.`;
// Should report updating with new format
expect(consoleSpy).toHaveBeenCalledWith(
'Updated OpenSpec instructions (README.md)\nUpdated AI tool files: CLAUDE.md'
'Updated OpenSpec instructions (AGENTS.md)\nUpdated AI tool files: CLAUDE.md'
);
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();
@@ -116,16 +200,16 @@ More content after.`;
}
});
it('should update README.md in openspec directory', async () => {
it('should update AGENTS.md in openspec directory', async () => {
// Execute update command
await updateCommand.execute(testDir);
// Check that README.md was created/updated
const readmePath = path.join(testDir, 'openspec', 'README.md');
const fileExists = await FileSystemUtils.fileExists(readmePath);
// Check that AGENTS.md was created/updated
const agentsPath = path.join(testDir, 'openspec', 'AGENTS.md');
const fileExists = await FileSystemUtils.fileExists(agentsPath);
expect(fileExists).toBe(true);
const content = await fs.readFile(readmePath, 'utf-8');
const content = await fs.readFile(agentsPath, 'utf-8');
expect(content).toContain('# OpenSpec Instructions');
});
@@ -144,22 +228,31 @@ More content after.`;
const claudePath = path.join(testDir, 'CLAUDE.md');
await fs.writeFile(claudePath, '<!-- OPENSPEC:START -->\nOld\n<!-- OPENSPEC:END -->');
await fs.chmod(claudePath, 0o444); // Read-only
const consoleSpy = vi.spyOn(console, 'log');
const errorSpy = vi.spyOn(console, 'error');
const originalWriteFile = FileSystemUtils.writeFile.bind(FileSystemUtils);
const writeSpy = vi.spyOn(FileSystemUtils, 'writeFile').mockImplementation(async (filePath, content) => {
if (filePath.endsWith('CLAUDE.md')) {
throw new Error('EACCES: permission denied, open');
}
return originalWriteFile(filePath, content);
});
// Execute update command - should not throw
await updateCommand.execute(testDir);
// Should report the failure
expect(errorSpy).toHaveBeenCalled();
expect(consoleSpy).toHaveBeenCalledWith(
'Updated OpenSpec instructions (README.md)\nFailed to update: CLAUDE.md'
'Updated OpenSpec instructions (AGENTS.md)\nFailed to update: CLAUDE.md'
);
// Restore permissions for cleanup
await fs.chmod(claudePath, 0o644);
consoleSpy.mockRestore();
errorSpy.mockRestore();
writeSpy.mockRestore();
});
});
});
+79
View File
@@ -0,0 +1,79 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { promises as fs } from 'fs';
import path from 'path';
import os from 'os';
import { ViewCommand } from '../../src/core/view.js';
const stripAnsi = (input: string): string => input.replace(/\u001b\[[0-9;]*m/g, '');
describe('ViewCommand', () => {
let tempDir: string;
let originalLog: typeof console.log;
let logOutput: string[] = [];
beforeEach(async () => {
tempDir = path.join(os.tmpdir(), `openspec-view-test-${Date.now()}`);
await fs.mkdir(tempDir, { recursive: true });
originalLog = console.log;
console.log = (...args: any[]) => {
logOutput.push(args.join(' '));
};
logOutput = [];
});
afterEach(async () => {
console.log = originalLog;
await fs.rm(tempDir, { recursive: true, force: true });
});
it('sorts active changes by completion percentage ascending with deterministic tie-breakers', async () => {
const changesDir = path.join(tempDir, 'openspec', 'changes');
await fs.mkdir(changesDir, { recursive: true });
await fs.mkdir(path.join(changesDir, 'gamma-change'), { recursive: true });
await fs.writeFile(
path.join(changesDir, 'gamma-change', 'tasks.md'),
'- [x] Done\n- [x] Also done\n- [ ] Not done\n'
);
await fs.mkdir(path.join(changesDir, 'beta-change'), { recursive: true });
await fs.writeFile(
path.join(changesDir, 'beta-change', 'tasks.md'),
'- [x] Task 1\n- [ ] Task 2\n'
);
await fs.mkdir(path.join(changesDir, 'delta-change'), { recursive: true });
await fs.writeFile(
path.join(changesDir, 'delta-change', 'tasks.md'),
'- [x] Task 1\n- [ ] Task 2\n'
);
await fs.mkdir(path.join(changesDir, 'alpha-change'), { recursive: true });
await fs.writeFile(
path.join(changesDir, 'alpha-change', 'tasks.md'),
'- [ ] Task 1\n- [ ] Task 2\n'
);
const viewCommand = new ViewCommand();
await viewCommand.execute(tempDir);
const activeLines = logOutput
.map(stripAnsi)
.filter(line => line.includes('◉'));
const activeOrder = activeLines.map(line => {
const afterBullet = line.split('◉')[1] ?? '';
return afterBullet.split('[')[0]?.trim();
});
expect(activeOrder).toEqual([
'alpha-change',
'beta-change',
'delta-change',
'gamma-change'
]);
});
});