Repository navigation
Support nested/namespaced capability paths consistently across validate, show, apply, and archive #1353
Description
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-changereports thespecsartifact as complete because the artifact
glob finds the file.openspec show my-change --json --deltas-onlyreturnsdeltaCount: 0.openspec validate my-changereports that the change has no deltas.archive/applydoes 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()readsspecs/<first-level-entry>/spec.md.findSpecUpdates()inspecs-applyreads and writes onlyspecs/<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-capabilitycapability
Then run:
openspec status --change nested-test
openspec show nested-test --json --deltas-only
openspec validate nested-test
Expected behavior
OpenSpec should:
- Discover
specs/**/spec.mdrecursively. - Return
platform/example-capabilityas the capability identifier. - 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
- Change delta:
- 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
fix(specs): surface non-ENOENT errors in spec discovery
fix(specs): discover nested spec paths recursively across parse, appl…
fix(security): repair four regressions the hardening introduced
fix(security): harden CLI against hostile-repository inputs and clear…