mirror of
https://github.com/Fission-AI/OpenSpec.git
synced 2026-10-04 06:18:24 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b0f494754 | ||
|
|
21a0e74b74 | ||
|
|
485ef07ec7 | ||
|
|
ce5ceadbe7 | ||
|
|
9a173b917c | ||
|
|
79baabbed1 | ||
|
|
818a5922ce | ||
|
|
d8d2930182 | ||
|
|
6af6e0ccb6 | ||
|
|
50e6660018 | ||
|
|
4bbb52dda4 | ||
|
|
4a0ae49b6e | ||
|
|
5b2049aedf | ||
|
|
332020ac6b | ||
|
|
646c516b0d |
@@ -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
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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"
|
||||
────────────────────────────────────────────────────────────
|
||||
```
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
@@ -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
@@ -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 };
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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'
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user