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
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
- **COMPATIBILITY**: Existing flat spec IDs remain valid — no migration required. Tools that treat spec IDs as opaque strings need no changes. Tools that parse or construct spec ID structure (e.g., splitting on `-` to infer hierarchy) must be updated to handle `/`-separated IDs.

## 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]:
```
52 changes: 52 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,52 @@
## 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/` at a segment boundary (i.e., the character after the prefix must be a path separator or the prefix must end with `/`)
- **AND** display them with their full hierarchical IDs
- **AND** `cli/` matches `cli/show` and `cli/bar/baz` but NOT `client/foo`

### 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
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