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
27 changed files with 331 additions and 72 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

@@ -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,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.
@@ -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"
+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",
+1 -1
View File
@@ -132,7 +132,7 @@ 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.
+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
+3 -3
View File
@@ -1,4 +1,4 @@
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';
@@ -12,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',
+18 -13
View File
@@ -1,20 +1,25 @@
export type SlashCommandId = 'proposal' | 'apply' | 'archive';
const baseGuardrails = `**Guardrails**
- Default to <100 lines of new code, single-file solutions, and avoid new frameworks unless OpenSpec data requires it.
- Use pnpm for Node.js tooling and keep changes scoped to the requested outcome.`;
- 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- Ask up to two clarifying questions if the request is ambiguous before editing files.`;
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\` to understand current work and capabilities.
2. Choose a unique verb-led \`change-id\` and scaffold \`proposal.md\`, \`tasks.md\`, and optional \`design.md\` under \`openspec/changes/<id>/\`.
3. Draft spec deltas in \`changes/<id>/specs/\` using \`## ADDED|MODIFIED|REMOVED Requirements\` with at least one \`#### Scenario:\` per requirement.
4. Validate with \`openspec validate <id> --strict\` and resolve every issue before sharing the proposal.`;
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.`;
- 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.
@@ -26,13 +31,13 @@ const applyReferences = `**Reference**
- Use \`openspec show <id> --json --deltas-only\` if you need additional context from the proposal while implementing.`;
const archiveSteps = `**Steps**
1. Confirm deployment is complete, then move \`changes/<id>/\` to \`changes/archive/YYYY-MM-DD-<id>/\`.
2. Update \`openspec/specs/\` to capture production behaviour, editing existing capabilities before creating new ones.
3. Run \`openspec archive <id> --skip-specs\` only for tooling-only work; otherwise ensure spec deltas are committed.
4. Re-run \`openspec validate --strict\` and review with \`openspec show <id>\` to verify archive changes.`;
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**
- Cross-check capabilities with \`openspec list --specs\` and resolve any outstanding validation issues before finishing.`;
- 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'),
+8 -5
View File
@@ -1,7 +1,7 @@
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';
@@ -16,9 +16,9 @@ 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();
@@ -34,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) {
@@ -60,7 +63,7 @@ export class UpdateCommand {
}
// 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(', ')}`);
+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);
+9 -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');
@@ -110,7 +110,8 @@ describe('InitCommand', () => {
const archiveContent = await fs.readFile(claudeArchive, 'utf-8');
expect(archiveContent).toContain('name: OpenSpec: Archive');
expect(archiveContent).toContain('openspec archive <id> --skip-specs');
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 () => {
+24 -15
View File
@@ -56,7 +56,7 @@ 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();
});
@@ -86,7 +86,7 @@ Old slash content
expect(updated).not.toContain('Old slash content');
expect(consoleSpy).toHaveBeenCalledWith(
'Updated OpenSpec instructions (README.md)\nUpdated slash commands: .claude/commands/openspec/proposal.md'
'Updated OpenSpec instructions (AGENTS.md)\nUpdated slash commands: .claude/commands/openspec/proposal.md'
);
consoleSpy.mockRestore();
@@ -128,7 +128,7 @@ Old body
expect(updated).not.toContain('Old body');
expect(consoleSpy).toHaveBeenCalledWith(
'Updated OpenSpec instructions (README.md)\nUpdated slash commands: .cursor/commands/openspec-apply.md'
'Updated OpenSpec instructions (AGENTS.md)\nUpdated slash commands: .cursor/commands/openspec-apply.md'
);
consoleSpy.mockRestore();
@@ -140,7 +140,7 @@ Old body
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();
});
@@ -158,7 +158,7 @@ Old body
// 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();
});
@@ -200,16 +200,16 @@ Old content
}
});
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');
});
@@ -228,22 +228,31 @@ Old content
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'
]);
});
});