Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tidy-change-approval.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@fission-ai/openspec': minor
---

Create changes in `changes/proposed/` and teach agents to move approved plans into `changes/approved/`. CLI commands resolve both folders and existing flat changes by name; updating leaves old changes in place. Archive rejects changes still in proposed.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,16 @@ AI: Let me look at your styling setup...
You: Yes, let's do it.

You: /opsx:propose add-dark-mode
AI: Created openspec/changes/add-dark-mode/
AI: Created openspec/changes/proposed/add-dark-mode/
✓ proposal.md — why we're doing this, what's changing
✓ specs/ — requirements and scenarios
✓ design.md — technical approach
✓ tasks.md — implementation checklist
Ready for implementation!

You: /opsx:apply
AI: Implementing tasks...
AI: Moved to openspec/changes/approved/add-dark-mode/
Implementing tasks...
✓ 1.1 Add theme context provider
✓ 1.2 Create toggle component
✓ 2.1 Add CSS variables
Expand Down
16 changes: 14 additions & 2 deletions docs/concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ A change is a proposed modification to your system, packaged as a folder with ev
### Change Structure

```
openspec/changes/add-dark-mode/
openspec/changes/proposed/add-dark-mode/
├── proposal.md # Why and what
├── design.md # How (technical approach)
├── tasks.md # Implementation checklist
Expand All @@ -201,6 +201,18 @@ Each change is self-contained. It has:
- **Delta specs** — specifications for what's being added, modified, or removed
- **Metadata** — optional configuration for this specific change

### Change Directories

- **`changes/proposed/`**: New plans awaiting approval.
- **`changes/approved/`**: Plans you have approved in conversation or selected for apply. Your agent moves the whole change folder here.
- **`changes/archive/`**: Archived changes.

Approving a plan can leave implementation for later. Existing flat `changes/<name>/` folders stay usable after an update; your agent moves one when you approve or apply it. Commands still accept the change name, such as `openspec status --change add-dark-mode`.

Archive accepts approved changes and existing flat changes. It rejects changes still in `proposed/`.

Scripts should use the resolved paths returned by `status --json`. Older CLI versions do not discover changes inside the new folders.

### Why Changes Are Folders

Packaging a change as a folder has several benefits:
Expand Down Expand Up @@ -514,7 +526,7 @@ openspec/
│ └── auth/
│ └── spec.md ◄────────────────┐
└── changes/ │
└── add-2fa/ │
└── approved/add-2fa/ │
├── proposal.md │
├── design.md │ merge
├── tasks.md │
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-09-08
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
## Context

New changes belong in proposed; user approval or apply moves them into approved. Existing flat changes stay readable.

## Goals / Non-Goals

The directory carries approval. The agent performs moves and handles conversational intent. No approval command, migration, metadata, approval column, or automated legacy classification.

## Decisions

- Share a small discovery/resolution helper across existing CLI consumers. Search only flat changes and the two explicit containers; keep bare change names and existing command output. Reject ambiguous names instead of choosing a different change silently.
- Create new changes under proposed. Keep old changes where they are; users and their agents can see them during ordinary scanning. Update never mass-moves them.
- Put brief approval guidance in the shared workflow instructions: use the resolved planning home, move the finished change on explicit approval or apply, preserve its contents, and refresh paths. Approval alone does not start implementation. Agents use their available filesystem tools on Windows, macOS, and Linux.
- Archive approved changes; reject proposed changes. Preserve archive behavior for existing flat changes and keep the destination unchanged. Use native Node.js paths in CLI lookup; preserve existing traversal and linked-path protections.

## Risks / Trade-offs

Bare names can collide across directories after a merge; report the conflicting paths. Existing changes named proposed or approved can collide with container names; preserve marked change directories and report a conflict before using them as containers. Older CLIs and scripts constructing flat paths need updating. Conversational behavior relies on the user's LLM following workflow instructions.

## Migration Plan

No migration. Ship lookup, creation, and workflow instructions together.
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
## Why

Shared change directories mix ideas awaiting review with plans approved for implementation. Developers need to see that distinction before treating a change as pending work.

## What Changes

- New changes move from `changes/<name>/` to `changes/proposed/<name>/`. **BREAKING** for scripts that construct paths; change names and CLI selectors stay the same.
- Explicit conversational approval of a finished plan, or invoking the apply workflow for it, moves the change to `changes/approved/<name>/` before implementation. Approval alone can leave implementation for later.
- Commands find changes in either directory. The directory is the source of approval state.
- Updating OpenSpec preserves existing flat changes and the agent moves them to `approved/` only upon explicit approval or apply. Existing archives remain in `changes/archive/`.
- The user's agent performs the move using its filesystem tools. No approval command, metadata, migration, or legacy-status UI.
- Changes follow proposed → approved → archive. Archive rejects proposed changes; existing flat changes retain their archive behavior.

## Capabilities

### New Capabilities

- `change-approval`: Approval transition, agent workflow triggers, location-aware command behavior, and legacy compatibility.

### Modified Capabilities

- `change-creation`: Create proposed changes and prevent duplicate active names across locations.
- `cli-list`: Discover all active locations.
- `openspec-conventions`: Document proposed, approved, legacy, and archived change locations.

## Impact

Change creation and discovery, planning-home resolution, commands that read or archive changes, generated workflow instructions, shell completion, and user documentation need consistent paths. No new dependency or metadata state machine is needed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
## Purpose

Distinguish changes awaiting review from changes approved for implementation while preserving access to existing plans.

## ADDED Requirements

### Requirement: Explicit approval transition
OpenSpec's agent workflows SHALL instruct the agent to move a finished proposed or legacy change into `changes/approved/<name>/` within its selected planning root, preserving its name and contents. Approval SHALL be determined by directory location.

#### Scenario: Approve a finished plan
- **WHEN** the user approves a change whose schema-required planning artifacts are complete
- **THEN** the agent moves the entire change into `approved/` and reports its resolved location
- **AND** task progress and artifact contents remain unchanged

#### Scenario: Approval without implementation
- **WHEN** the user approves the finished plan in conversation but leaves implementation for later
- **THEN** the agent moves the directory and leaves implementation tasks untouched

#### Scenario: Apply implies approval
- **WHEN** the user invokes the apply workflow for a finished proposed or legacy change
- **THEN** the agent approves that change before implementation and refreshes its resolved artifact paths

#### Scenario: Planning is incomplete
- **WHEN** approval or apply is requested for a change missing required planning artifacts
- **THEN** the agent reports the missing artifacts and the change stays at its current location

#### Scenario: Finishing a proposal is not approval
- **WHEN** the agent completes planning without explicit user approval or an apply request
- **THEN** the change remains proposed

#### Scenario: Approval is repeated
- **WHEN** an already approved change is approved again
- **THEN** the agent uses the existing approved location

#### Scenario: Move cannot be completed
- **WHEN** the destination conflicts or a filesystem error prevents approval
- **THEN** the agent reports the failure, preserves existing content, and the agent pauses before implementation

#### Scenario: Approval on Windows or in a store
- **WHEN** a user approves a change on Windows, macOS, or Linux, including with a selected store
- **THEN** it moves within the selected planning root using native paths, including roots containing spaces

### Requirement: Stable change identity
OpenSpec SHALL resolve active changes by their existing bare name across proposed, approved, and legacy locations for listing, showing, validation, status, instructions, view, completion, and archive. Resolved artifact paths SHALL identify the actual location.

#### Scenario: Commands follow approval
- **WHEN** a change moves from proposed to approved
- **THEN** existing name-based commands continue to address the same artifacts and report their new paths

#### Scenario: Read-only inspection
- **WHEN** a user lists changes or requests status, validation, or apply instructions
- **THEN** the change's approval state and location remain unchanged

#### Scenario: Duplicate names from a merge
- **WHEN** the same active name exists in more than one supported location
- **THEN** commands report the conflicting paths and require the collision to be resolved before acting on that name

#### Scenario: Archive an approved or legacy change
- **WHEN** a user archives an approved or legacy change
- **THEN** existing archive checks and spec synchronization behavior apply to its resolved directory
- **AND** the destination remains `changes/archive/<archive-name>/`

#### Scenario: A proposed change cannot be archived
- **WHEN** a user tries to archive a change still in proposed
- **THEN** OpenSpec leaves it in place and asks the user to approve or apply it first

### Requirement: Preserve legacy changes on update
Updating OpenSpec SHALL preserve flat `changes/<name>/` directories and make them accessible by name without assigning approval.

#### Scenario: Update an existing project
- **WHEN** OpenSpec is updated in a project containing flat changes and archives
- **THEN** existing change and archive contents and locations are preserved
- **AND** new changes use `proposed/`

#### Scenario: Approve a legacy change
- **WHEN** a finished legacy change is explicitly approved or selected for apply
- **THEN** it moves directly into `approved/` using the same approval behavior as a proposed change
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
## MODIFIED Requirements

### Requirement: Change Creation
The system SHALL provide a function to create new change directories programmatically in the proposed location of the selected planning root.

#### Scenario: Create change
- **WHEN** `createChange(projectRoot, 'add-auth')` is called
- **THEN** the system creates `openspec/changes/proposed/add-auth/` directory

#### Scenario: Duplicate change rejected
- **WHEN** `createChange(projectRoot, 'add-auth')` is called and that name already exists in proposed, approved, or legacy active changes
- **THEN** the system throws an error indicating the change already exists

#### Scenario: Creates parent directories if needed
- **WHEN** `createChange(projectRoot, 'add-auth')` is called and the proposed parent directory does not exist
- **THEN** the system creates the full path including parent directories

#### Scenario: Invalid change name rejected
- **WHEN** `createChange(projectRoot, 'Add Auth')` is called with an invalid name
- **THEN** the system throws a validation error

#### Scenario: Cross-platform creation
- **WHEN** a change is created on Windows, macOS, or Linux
- **THEN** the proposed directory and returned artifact paths use the platform's native path handling
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
## MODIFIED Requirements

### Requirement: Command Execution
The command SHALL scan and analyze either active changes or specs based on the selected mode.

#### Scenario: Scanning for changes (default)
- **WHEN** `openspec list` is executed without flags
- **THEN** scan proposed, approved, and legacy active change directories in the selected planning root
- **AND** exclude archive, hidden directories, and the proposed and approved container directories themselves from results
- **AND** parse each change's `tasks.md` file to count task completion

#### Scenario: Scanning for specs
- **WHEN** `openspec list --specs` is executed
- **THEN** scan the `openspec/specs/` directory for capabilities
- **AND** read each capability's `spec.md`
- **AND** parse requirements to compute requirement counts

#### Scenario: Windows discovery
- **WHEN** changes exist in both proposed and approved on Windows
- **THEN** both are listed by their bare names with the same approval and progress semantics as on macOS and Linux
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
## MODIFIED Requirements

### Requirement: Project Structure
An OpenSpec project SHALL maintain a consistent directory structure for specifications and changes, using native filesystem paths on Windows, macOS, and Linux.

#### Scenario: Initializing project structure
- **WHEN** an OpenSpec project is initialized
- **THEN** its change organization SHALL have this structure:
```text
openspec/
├── specs/
│ └── <capability-path>/
│ ├── spec.md
│ └── design.md # Optional capability design
└── changes/
├── proposed/ # Awaiting approval
│ └── <change-name>/
├── approved/ # Approved for implementation
│ └── <change-name>/
└── archive/ # Archived changes
└── YYYY-MM-DD-<name>/
```
- **AND** change directories contain their schema's planning artifacts, including proposal, tasks, optional design, and delta specs for the default workflow
- **AND** project context and agent instruction files follow the existing initialization rules

#### Scenario: Existing flat changes
- **WHEN** a project contains `changes/<change-name>/` from an earlier version
- **THEN** it remains a supported legacy location until explicitly moved by approval or archive
15 changes: 15 additions & 0 deletions openspec/changes/approved/organize-changes-by-approval/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
## 1. Directory support

- [x] 1.1 Share active-change discovery and resolution across CLI consumers; verify proposed, approved, flat, duplicate, and linked-path cases.
- [x] 1.2 Create proposed changes and preserve old directories on update; verify creation and mixed-layout command behavior.

## 2. Agent workflow

- [x] 2.1 Add concise instructions for agent-driven approval moves and refreshed paths; regenerate skills and verify rendered workflow coverage.

## 3. Verification and documentation

- [x] 3.1 Document the directory convention and add a changeset; verify examples against the built CLI.
- [x] 3.2 Run build, lint, focused tests, and end-to-end move/archive checks; include cross-platform path coverage and report Windows CI as pending if unavailable locally.

Verification: build, lint, and strict change validation pass. Full-suite run: 4,426 passed; four outdated creation-path fixtures corrected, then all 375 tests in the affected suites passed. After enforcing approved-only archive, all 259 tests in the archive, store journey, and template suites passed. Windows CI has not run locally. Agent instructions were checked through generated-template parity and code review; conversational behavior was not evaluated across LLM providers.
1 change: 1 addition & 0 deletions skills/openspec-apply-change/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ metadata:
Implement tasks from an OpenSpec change.

**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store <id>` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store <id>` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "<name>" --json --store "<id>"`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root.
When the user approves a finished plan in chat or invokes apply, move its `changeRoot` into `<planningHome.changesDir>/approved/<name>` if not already there, then refresh status and instructions before any requested implementation; never archive a proposed change.

**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store <id>` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it.

Expand Down
1 change: 1 addition & 0 deletions skills/openspec-archive-change/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ metadata:
Archive a completed change in the experimental workflow.

**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store <id>` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store <id>` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "<name>" --json --store "<id>"`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root.
When the user approves a finished plan in chat or invokes apply, move its `changeRoot` into `<planningHome.changesDir>/approved/<name>` if not already there, then refresh status and instructions before any requested implementation; never archive a proposed change.

**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store <id>` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it.

Expand Down
Loading