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 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
27 changed files with 612 additions and 67 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
+10 -2
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.**
@@ -92,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

+8 -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
@@ -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.
@@ -1,16 +1,16 @@
# Implementation Tasks
## 1. Templates and Configurators
- [ ] 1.1 Create shared templates for the Proposal, Apply, and Archive commands with instructions for each workflow stage from `openspec/README.md`.
- [ ] 1.2 Implement a `SlashCommandConfigurator` base and tool-specific configurators for Claude Code and Cursor.
- [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
- [ ] 2.1 Generate `.claude/commands/openspec/{proposal,apply,archive}.md` during `openspec init` using shared templates.
- [ ] 2.2 Update existing `.claude/commands/openspec/*` files during `openspec update`.
- [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
- [ ] 3.1 Generate `.cursor/commands/{openspec-proposal,openspec-apply,openspec-archive}.md` during `openspec init` using shared templates.
- [ ] 3.2 Update existing `.cursor/commands/*` files during `openspec update`.
- [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
- [ ] 4.1 Add tests verifying slash command files are created and updated correctly.
- [x] 4.1 Add tests verifying slash command files are created and updated correctly.
@@ -1,22 +1,22 @@
# Update Agent Instruction File Name - Tasks
## 1. Rename Instruction File
- [ ] Rename `openspec/README.md` to `openspec/AGENTS.md`
- [ ] Update root references to new path
- [x] Rename `openspec/README.md` to `openspec/AGENTS.md`
- [x] Update root references to new path
## 2. Update Templates
- [ ] Rename `src/core/templates/readme-template.ts` to `agents-template.ts`
- [ ] Update exported constant from `readmeTemplate` to `agentsTemplate`
- [x] Rename `src/core/templates/readme-template.ts` to `agents-template.ts`
- [x] Update exported constant from `readmeTemplate` to `agentsTemplate`
## 3. Adjust CLI Commands
- [ ] Modify `openspec init` to generate `AGENTS.md`
- [ ] Update `openspec update` to refresh `AGENTS.md`
- [ ] Ensure CLAUDE.md markers link to `@openspec/AGENTS.md`
- [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
- [ ] Modify `cli-init` spec to reference `AGENTS.md`
- [ ] Modify `cli-update` spec to reference `AGENTS.md`
- [ ] Modify `openspec-conventions` spec to include `AGENTS.md` in project structure
- [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
- [ ] `pnpm test`
- [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"
+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
+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'));
}
+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;
}
}
}
+94 -10
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');
});
@@ -162,7 +246,7 @@ More content after.`;
// 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
@@ -171,4 +255,4 @@ More content after.`;
errorSpy.mockRestore();
writeSpy.mockRestore();
});
});
});