Skip to content

Support nested/namespaced capability paths consistently across validate, show, apply, and archive #1353

Closed

Description

@racase

Motivation

A shared OpenSpec store may contain specifications from multiple applications or bounded contexts.
To prevent capability-name collisions and keep the store navigable, we need to organize capabilities
using namespaces:

openspec/specs/
├── platform/
│ └── platform-session-layout/
│ └── spec.md
└── mobile/
└── mobile-session-layout/
└── spec.md

Changes use the corresponding relative paths:

openspec/changes/my-change/specs/platform/platform-session-layout/spec.md

The capability identifier would be platform/platform-session-layout.

This is particularly useful when multiple repositories write to the same OpenSpec store and may
otherwise create colliding capability names.

Current behavior

OpenSpec 1.5.0 only discovers delta specs located exactly one directory below specs/.

For example, this file is not parsed:

openspec/changes/my-change/specs/platform/platform-session-layout/spec.md

Observed results:

  • openspec status --change my-change reports the specs artifact as complete because the artifact
    glob finds the file.
  • openspec show my-change --json --deltas-only returns deltaCount: 0.
  • openspec validate my-change reports that the change has no deltas.
  • archive/apply does not merge the nested delta into the corresponding main spec.

OpenSpec 1.6.0 adds recursive delta discovery to part of the validation flow (#1280), but the
published package still contains one-level traversal in other important paths.

In particular:

  • ChangeParser.parseDeltaSpecs() reads specs/<first-level-entry>/spec.md.
  • findSpecUpdates() in specs-apply reads and writes only specs/<first-level-entry>/spec.md.
  • Main-spec discovery also assumes one directory level.

As a result, validation, show, list, apply, sync, and archive do not use the same discovery
semantics.

Minimal reproduction

Create a valid delta at:

openspec/changes/nested-test/specs/platform/example-capability/spec.md

with:

ADDED Requirements

Requirement: Nested capability works

The system SHALL discover capabilities stored below namespace directories.

Scenario: Validate nested delta

  • WHEN the user validates the change
  • THEN OpenSpec detects the platform/example-capability capability

Then run:

openspec status --change nested-test
openspec show nested-test --json --deltas-only
openspec validate nested-test

Expected behavior

OpenSpec should:

  1. Discover specs/**/spec.md recursively.
  2. Return platform/example-capability as the capability identifier.
  3. Preserve the relative path when applying or archiving:
    • Change delta:
      changes/nested-test/specs/platform/example-capability/spec.md
    • Main spec:
      specs/platform/example-capability/spec.md
  4. Apply the same discovery rules consistently across:
    • status
    • show
    • validate
    • list/view
    • sync
    • apply
    • archive

Actual behavior

Some artifact-resolution code discovers the nested file through glob patterns, while the delta
parser and spec-application code only inspect immediate child directories. This produces
inconsistent results and can cause an archive to omit valid nested deltas.

Possible solution

Introduce a shared recursive spec-discovery helper used by all commands and workflows.

Conceptually:

interface DiscoveredSpec {
  capabilityId: string;
  specFile: string;
  relativeDirectory: string;
}

    cwd: specsRoot,
    onlyFiles: true,
    followSymbolicLinks: false,
  });

  return files.map((relativeFile) => {
    const relativeDirectory = path.dirname(relativeFile);
    const capabilityId = relativeDirectory
      .split(path.sep)
      .join("/");

    return {
      capabilityId,
      relativeDirectory,
      specFile: path.join(specsRoot, relativeFile),
    };
  });
}

For apply/archive, the target should preserve relativeDirectory:

const targetFile = path.join(
  mainSpecsDir,
  discovered.relativeDirectory,
  "spec.md",
);

The implementation should also:

- Reject absolute paths and .. traversal.
- Normalize capability IDs to forward slashes on every platform.
- Detect duplicate normalized capability IDs.
- Avoid following symlinks outside the OpenSpec root.
- Add Windows and POSIX tests.
- Cover nested paths in validate, show, list, sync, apply, and archive tests.

## Environment

- OpenSpec installed: 1.5.0
- Latest version inspected: 1.6.0
- OS: Windows
- Store mode: shared standalone OpenSpec store

Activity

  1. added a commit that references this issue on Jul 17, 2026
    3fdd2f2
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions