Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 2 additions & 0 deletions openspec/changes/hierarchical-specs/.openspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-03-13
74 changes: 74 additions & 0 deletions openspec/changes/hierarchical-specs/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
## Context

OpenSpec currently stores specs in a flat directory structure at `openspec/specs/<spec-id>/spec.md`. Spec IDs are simple strings (e.g., `cli-show`, `schema-resolution`). The `getSpecIds()` function in `item-discovery.ts` reads only one level deep, and all path construction uses `path.join(SPECS_DIR, specId, 'spec.md')`.

As the project has grown to 38 specs, naming conventions like `cli-*`, `schema-*` have emerged organically — encoding hierarchy in flat names. This change introduces proper directory nesting so specs can be organized as `cli/show`, `schema/resolution`, etc.

## Goals / Non-Goals

**Goals:**
- Support arbitrary nesting depth for specs (e.g., `domain/project/feature`)
- Make spec IDs path-based, using `/` as the separator
- Maintain full backward compatibility with existing flat spec IDs
- Update all CLI commands to work with hierarchical spec IDs
- Support subtree operations (e.g., listing all specs under `cli/`)

**Non-Goals:**
- Automatic migration of existing flat specs to hierarchical structure — users migrate at their own pace
- Cross-references or symbolic links between specs
- Spec inheritance or composition across hierarchy levels
- Namespace-level metadata or configuration (e.g., `cli/.openspec.yaml`)

## Decisions

### Decision 1: Spec IDs use forward slash as separator, regardless of OS

Spec IDs use `/` as the canonical separator (e.g., `cli/show`), even on Windows. Internally, `path.join` is used for filesystem operations, but IDs are always stored and displayed with `/`.

**Why**: Consistent cross-platform behavior. Spec IDs appear in change files, JSON output, and documentation — they must be portable. This matches how Go import paths and npm package scopes work.

**Alternative considered**: Using the OS path separator — rejected because spec IDs would differ across platforms, breaking change portability.

### Decision 2: Recursive discovery with `spec.md` as the leaf marker

`getSpecIds()` walks the directory tree recursively. Any directory containing `spec.md` is a spec — its ID is the relative path from `openspec/specs/` to that directory. Directories without `spec.md` are treated as organizational containers.

**Why**: Simple, unambiguous detection. No configuration files needed at intermediate directories. The existing convention (`spec.md` = spec exists) naturally extends to nested structures.

**Alternative considered**: Requiring a manifest file listing specs — rejected as it adds maintenance burden and a sync problem.

### Decision 3: Flat and hierarchical specs coexist

Both `openspec/specs/cli-show/spec.md` (flat) and `openspec/specs/cli/show/spec.md` (hierarchical) are valid. They are different specs with different IDs (`cli-show` vs `cli/show`).

**Why**: Zero-migration-cost adoption. Teams can gradually reorganize without a flag day. Existing tooling and change references continue to work.

**Alternative considered**: Deprecating flat structure — rejected as it forces migration and breaks existing changes/archives.

### Decision 4: Subtree filtering uses prefix matching on spec IDs

`openspec spec list cli/` returns all specs whose ID starts with `cli/`. This is a simple string prefix match on the canonical `/`-separated ID.

**Why**: Intuitive UX — `cli/` means "everything under cli". No glob syntax needed for the common case.

### Decision 5: Delta specs in changes mirror the hierarchy

Change delta specs at `changes/<name>/specs/` use the same hierarchy. For example, modifying spec `cli/show` means creating `changes/<name>/specs/cli/show/spec.md`.

**Why**: Consistent mental model. The change's `specs/` directory is a mirror of the main `openspec/specs/` structure. The archive command can use the same relative path logic for both.

### Decision 6: Fuzzy matching extends to hierarchical IDs

The existing Levenshtein-based suggestion system works on the full path-based ID string. Additionally, partial path matching is supported — typing `show` suggests `cli/show` if it exists.

**Why**: Discoverability. Users may not know the full path. Matching on the leaf segment (last component) helps find specs without knowing the full hierarchy.

## Risks / Trade-offs

**[Risk] Ambiguity between flat and nested IDs** → Flat ID `cli-show` and nested ID `cli/show` are distinct specs. If both exist, commands resolve them independently. Documentation should clarify naming conventions to avoid confusion.

**[Risk] Deep nesting becomes unwieldy** → No technical limit on depth, but deeply nested IDs (`a/b/c/d/e/spec.md`) are cumbersome to type. Mitigation: document recommended max depth of 3 levels; fuzzy matching reduces typing burden.

**[Risk] Performance with large spec trees** → Recursive directory walking is slower than single-level readdir. Mitigation: `fast-glob` (already a dependency) can handle thousands of entries efficiently. Benchmark if >500 specs.

**[Risk] Cross-platform path handling bugs** → Mixing `/` in IDs with OS-specific `path.join` is error-prone. Mitigation: centralize ID↔path conversion in a single utility (`specIdToPath` / `pathToSpecId`), and add Windows path tests as per existing config rules.
36 changes: 36 additions & 0 deletions openspec/changes/hierarchical-specs/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
## Why

OpenSpec stores all specs in a flat structure under `openspec/specs/`. As projects grow, this becomes difficult to organize and navigate. Teams working on different domains or features resort to long, prefixed names (e.g., `cli-show`, `cli-validate`, `schema-fork-command`) to avoid collisions and convey hierarchy through naming conventions alone. A proper hierarchical structure would make specs more discoverable, reduce naming collisions, and let teams organize specs by domain, project, or feature.

## What Changes

- Spec IDs become path-based (e.g., `cli/show` instead of `cli-show`), supporting arbitrary nesting depth
- `getSpecIds()` in `item-discovery.ts` becomes recursive, discovering specs at any depth
- All CLI commands that resolve spec IDs (`spec show`, `spec list`, `spec validate`, `show`, `validate`, `list`, `view`) accept path-based spec IDs
- Path construction throughout the codebase changes from `join(SPECS_DIR, specId, 'spec.md')` to handle `/`-separated IDs as nested directories
- `spec list` gains the ability to filter by subtree (e.g., `openspec spec list cli/` shows only CLI specs)
- Delta specs in changes mirror the hierarchical structure
- **BREAKING**: Existing flat spec IDs remain valid — no migration required. However, tools that parse spec IDs as simple strings may need updating.
Comment thread
victorhsb marked this conversation as resolved.
Outdated

## Capabilities

### New Capabilities
- `hierarchical-spec-discovery`: Recursive spec discovery that finds `spec.md` files at any nesting depth under `openspec/specs/`, deriving spec IDs from relative directory paths
- `hierarchical-spec-resolution`: Path-based spec ID resolution, allowing specs to be referenced as `domain/project/feature` with subtree filtering and disambiguation support

### Modified Capabilities
- `cli-spec`: Spec commands (`show`, `list`, `validate`) must accept path-based spec IDs and support subtree listing
- `cli-show`: Show command must resolve hierarchical spec IDs
- `cli-validate`: Validate command must resolve hierarchical spec IDs
- `cli-list`: List command must display specs with their full hierarchical paths and support subtree filtering
- `cli-view`: View/dashboard must display specs organized by hierarchy
- `cli-archive`: Archive must handle delta specs in nested directory structures

## Impact

- **Core**: `item-discovery.ts` (`getSpecIds`), path construction in `spec.ts`, `show.ts`, `validate.ts`
- **Display**: `list.ts` and `view.ts` need tree-aware rendering
- **Archive**: `specs-apply.ts` and `archive.ts` need to handle nested delta spec paths
- **Change specs**: Delta specs within changes (`changes/<name>/specs/`) mirror the hierarchy
- **Tests**: All spec-related tests need updating for nested path scenarios
- **Cross-platform**: Path handling must use `path.join`/`path.posix` correctly — spec IDs use `/` as separator regardless of OS
55 changes: 55 additions & 0 deletions openspec/changes/hierarchical-specs/specs/cli-archive/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
## MODIFIED Requirements

### Requirement: Spec Update Process

Before moving the change to archive, the command SHALL apply delta changes to main specs to reflect the deployed reality.

#### Scenario: Applying delta changes

- **WHEN** archiving a change with delta-based specs
- **THEN** recursively discover delta specs within the change's `specs/` directory
- **AND** parse and apply delta changes as defined in openspec-conventions
- **AND** validate all operations before applying
- **AND** create nested directories under `openspec/specs/` as needed for new hierarchical specs

#### Scenario: Applying hierarchical delta specs

- **WHEN** a change contains delta spec at `changes/<name>/specs/cli/show/spec.md`
- **THEN** apply the delta to `openspec/specs/cli/show/spec.md`
- **AND** create intermediate directories (`cli/`) if they do not exist

#### Scenario: Validating delta changes

- **WHEN** processing delta changes
- **THEN** perform validations as specified in openspec-conventions
- **AND** if validation fails, show specific errors and abort

#### Scenario: Conflict detection

- **WHEN** applying deltas would create duplicate requirement headers
- **THEN** abort with error message showing the conflict
- **AND** suggest manual resolution

### Requirement: Confirmation Behavior

The spec update confirmation SHALL provide clear visibility into changes before they are applied.

#### Scenario: Displaying confirmation with hierarchical paths

- **WHEN** prompting for confirmation
- **THEN** display a clear summary showing:
- Which specs will be created (new capabilities) with full hierarchical paths
- Which specs will be updated (existing capabilities) with full hierarchical paths
- The source path for each spec
- **AND** format the confirmation prompt as:
```
The following specs will be updated:

NEW specs to be created:
- cli/archive (from changes/add-archive-command/specs/cli/archive/spec.md)

EXISTING specs to be updated:
- cli/init (from changes/update-init-command/specs/cli/init/spec.md)

Update 2 specs and archive 'add-archive-command'? [y/N]:
```
51 changes: 51 additions & 0 deletions openspec/changes/hierarchical-specs/specs/cli-list/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
## 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 the `openspec/changes/` directory for change directories
- **AND** exclude the `archive/` subdirectory 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** recursively scan the `openspec/specs/` directory tree for capabilities at any depth
- **AND** read each capability's `spec.md`
- **AND** parse requirements to compute requirement counts

#### Scenario: Scanning for specs in subtree
- **WHEN** `openspec list --specs cli/` is executed
- **THEN** recursively scan only specs whose ID starts with `cli/`
- **AND** display them with their full hierarchical IDs

Comment thread
coderabbitai[bot] marked this conversation as resolved.
### Requirement: Output Format
The command SHALL display items in a clear, readable table format with mode-appropriate progress or counts.

#### Scenario: Displaying change list (default)
- **WHEN** displaying the list of changes
- **THEN** show a table with columns:
- Change name (directory name)
- Task progress (e.g., "3/5 tasks" or "✓ Complete")

#### Scenario: Displaying spec list
- **WHEN** displaying the list of specs
- **THEN** show a table with columns:
- Spec id (full hierarchical path, e.g., `cli/show` or `cli-show`)
- Requirement count (e.g., "requirements 12")

Comment on lines +25 to +38
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Table-only output requirement conflicts with machine-readable JSON mode.

This section says output “SHALL” be table format, but the command surface includes --json (see src/cli/index.ts Lines 173-190). Add an explicit exception/scenario for JSON output to avoid contradictory requirements.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openspec/changes/hierarchical-specs/specs/cli-list/spec.md` around lines 25 -
38, The spec currently mandates table output but the CLI supports a --json mode,
so add an explicit exception: update the "Displaying change list (default)" and
"Displaying spec list" scenarios in specs/cli-list/spec.md to state that when
the --json flag is present the command SHALL emit machine-readable JSON
(structured arrays/objects with fields for change name, task progress, spec id,
requirement count) instead of a human-readable table; reference the CLI option
--json (from the CLI entry point) as the trigger and ensure the new scenario
clarifies the JSON schema or refers to a separate JSON output schema section.

### Requirement: Sorting

The command SHALL maintain consistent ordering of items for predictable output.

#### Scenario: Ordering changes

- **WHEN** displaying multiple changes
- **THEN** sort them in alphabetical order by change name

#### Scenario: Ordering specs

- **WHEN** displaying multiple specs
- **THEN** sort them in alphabetical order by full spec ID
- **AND** hierarchical IDs sort naturally (e.g., `cli/archive` before `cli/show`)
50 changes: 50 additions & 0 deletions openspec/changes/hierarchical-specs/specs/cli-show/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
## MODIFIED Requirements

### Requirement: Top-level show command

The CLI SHALL provide a top-level `show` command for displaying changes and specs with intelligent selection.

#### Scenario: Interactive show selection

- **WHEN** executing `openspec show` without arguments
- **THEN** prompt user to select type (change or spec)
- **AND** display list of available items for selected type, including hierarchical spec IDs
- **AND** show the selected item's content

#### Scenario: Non-interactive environments do not prompt

- **GIVEN** stdin is not a TTY or `--no-interactive` is provided or environment variable `OPEN_SPEC_INTERACTIVE=0`
- **WHEN** executing `openspec show` without arguments
- **THEN** do not prompt
- **AND** print a helpful hint with examples for `openspec show <item>` or `openspec change/spec show`
- **AND** exit with code 1

#### Scenario: Direct item display

- **WHEN** executing `openspec show <item-name>`
- **THEN** automatically detect if item is a change or spec
- **AND** display the item's content
- **AND** use appropriate formatting based on item type

#### Scenario: Direct hierarchical spec display

- **WHEN** executing `openspec show cli/show`
- **THEN** detect that `cli/show` is a hierarchical spec ID
- **AND** resolve it at `openspec/specs/cli/show/spec.md`
- **AND** display the spec content

#### Scenario: Type detection and ambiguity handling

- **WHEN** executing `openspec show <item-name>`
- **THEN** if `<item-name>` uniquely matches a change or a spec, show that item
- **AND** if it matches both, print an ambiguity error and suggest `--type change|spec` or using `openspec change show`/`openspec spec show`
- **AND** if it matches neither, print not-found with nearest-match suggestions including hierarchical specs

#### Scenario: Explicit type override

- **WHEN** executing `openspec show --type change <item>`
- **THEN** treat `<item>` as a change ID and show it (skipping auto-detection)

- **WHEN** executing `openspec show --type spec <item>`
- **THEN** treat `<item>` as a spec ID and show it (skipping auto-detection)
- **AND** support hierarchical spec IDs (e.g., `openspec show --type spec cli/show`)
91 changes: 91 additions & 0 deletions openspec/changes/hierarchical-specs/specs/cli-spec/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
## MODIFIED Requirements

### Requirement: Spec Command

The system SHALL provide a `spec` command with subcommands for displaying, listing, and validating specifications.

#### Scenario: Show spec as JSON

- **WHEN** executing `openspec spec show init --json`
- **THEN** parse the markdown spec file
- **AND** extract headings and content hierarchically
- **AND** output valid JSON to stdout

#### Scenario: List all specs

- **WHEN** executing `openspec spec list`
- **THEN** recursively scan the openspec/specs directory tree
- **AND** return list of all available capabilities with their full hierarchical IDs
- **AND** support JSON output with `--json` flag

#### Scenario: List specs in subtree

- **WHEN** executing `openspec spec list cli/`
- **THEN** return only specs whose ID starts with `cli/`
- **AND** display them with their full hierarchical IDs

#### Scenario: Show hierarchical spec

- **WHEN** executing `openspec spec show cli/show`
- **THEN** resolve the spec at `openspec/specs/cli/show/spec.md`
- **AND** display the spec content

#### Scenario: Filter spec content

- **WHEN** executing `openspec spec show init --requirements`
- **THEN** display only requirement names and SHALL statements
- **AND** exclude scenario content

#### Scenario: Validate spec structure

- **WHEN** executing `openspec spec validate init`
- **THEN** parse the spec file
- **AND** validate against Zod schema
- **AND** report any structural issues

#### Scenario: Validate hierarchical spec

- **WHEN** executing `openspec spec validate cli/show`
- **THEN** resolve the spec at `openspec/specs/cli/show/spec.md`
- **AND** validate against Zod schema
- **AND** report any structural issues

### Requirement: Interactive spec show

The spec show command SHALL support interactive selection when no spec-id is provided.

#### Scenario: Interactive spec selection for show

- **WHEN** executing `openspec spec show` without arguments
- **THEN** display an interactive list of available specs, including hierarchical IDs
- **AND** allow the user to select a spec to show
- **AND** display the selected spec content
- **AND** maintain all existing show options (--json, --requirements, --no-scenarios, -r)

#### Scenario: Non-interactive fallback keeps current behavior

- **GIVEN** stdin is not a TTY or `--no-interactive` is provided or environment variable `OPEN_SPEC_INTERACTIVE=0`
- **WHEN** executing `openspec spec show` without a spec-id
- **THEN** do not prompt interactively
- **AND** print the existing error message for missing spec-id
- **AND** set non-zero exit code

### Requirement: Interactive spec validation

The spec validate command SHALL support interactive selection when no spec-id is provided.

#### Scenario: Interactive spec selection for validation

- **WHEN** executing `openspec spec validate` without arguments
- **THEN** display an interactive list of available specs, including hierarchical IDs
- **AND** allow the user to select a spec to validate
- **AND** validate the selected spec
- **AND** maintain all existing validation options (--strict, --json)

#### Scenario: Non-interactive fallback keeps current behavior

- **GIVEN** stdin is not a TTY or `--no-interactive` is provided or environment variable `OPEN_SPEC_INTERACTIVE=0`
- **WHEN** executing `openspec spec validate` without a spec-id
- **THEN** do not prompt interactively
- **AND** print the existing error message for missing spec-id
- **AND** set non-zero exit code
Loading