feat: Add SPECIFY_SPECS_DIR for centralized specs directory and worktree support#1579
feat: Add SPECIFY_SPECS_DIR for centralized specs directory and worktree support#1579alanmeadows wants to merge 6 commits intogithub:mainfrom
Conversation
Enable specs directory to be located outside the repository via the SPECIFY_SPECS_DIR environment variable. This enables: - Worktree workflows where specs are shared across worktrees - Spec-first development (create specs before branches) - Cross-feature visibility when working on multiple features - Project-wide shared context via _shared/ subdirectory Changes: - Add get_specs_dir()/Get-SpecsDir functions to common scripts - Update all hardcoded specs path references - Add SPECS_DIR to JSON output from check-prerequisites - Update all command templates to load _shared/ context 100% backward compatible - when SPECIFY_SPECS_DIR is not set, behavior is identical to current.
There was a problem hiding this comment.
Pull request overview
This PR adds support for centralized specs directories via the SPECIFY_SPECS_DIR environment variable and introduces a _shared/ subdirectory convention for project-wide standards. The changes enable powerful workflows for git worktrees, multi-feature development, and team collaboration.
Changes:
- Add
get_specs_dir()/Get-SpecsDirfunctions to bash and PowerShell common libraries to support external specs directories viaSPECIFY_SPECS_DIR - Update all scripts to use the new specs directory functions instead of hardcoded paths
- Add
SPECS_DIRto JSON outputs in check-prerequisites scripts for downstream consumption - Update 7 command templates to conditionally load project-wide context from
SPECS_DIR/_shared/directory
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| scripts/bash/common.sh | Add get_specs_dir() function to support SPECIFY_SPECS_DIR environment variable |
| scripts/bash/create-new-feature.sh | Use SPECIFY_SPECS_DIR environment variable for specs directory location |
| scripts/bash/check-prerequisites.sh | Include SPECS_DIR in JSON output for both paths-only and normal modes |
| scripts/powershell/common.ps1 | Add Get-SpecsDir function to support SPECIFY_SPECS_DIR environment variable |
| scripts/powershell/create-new-feature.ps1 | Use SPECIFY_SPECS_DIR environment variable for specs directory location |
| scripts/powershell/check-prerequisites.ps1 | Include SPECS_DIR in JSON output for both paths-only and normal modes |
| templates/commands/specify.md | Add step to load shared context from SPECS_DIR/_shared/ directory |
| templates/commands/plan.md | Add instruction to load shared context from SPECS_DIR/_shared/ directory |
| templates/commands/tasks.md | Add instruction to load shared context from SPECS_DIR/_shared/ directory |
| templates/commands/implement.md | Add instruction to load shared context from SPECS_DIR/_shared/ directory |
| templates/commands/clarify.md | Add instruction to load shared context from SPECS_DIR/_shared/ directory |
| templates/commands/checklist.md | Add instruction to load shared context from SPECS_DIR/_shared/ directory |
| templates/commands/analyze.md | Add instruction to load shared context from SPECS_DIR/_shared/ directory |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Add path validation for SPECIFY_SPECS_DIR (must be absolute, no ..) - Add json_escape helper function for safe JSON string encoding - Add SPECS_DIR to create-new-feature.sh/ps1 JSON output - Update specify.md template wording for clarity - Apply json_escape to all printf JSON outputs
|
All review feedback has been addressed in the latest commit: Path Validation (common.sh, common.ps1):
JSON Escaping (common.sh):
SPECS_DIR in JSON Output (create-new-feature.sh, create-new-feature.ps1):
Template Wording (specify.md):
|
- Use get_specs_dir/Get-SpecsDir consistently instead of raw env var access - Support relative paths by resolving against repo root - Add exit-on-failure checks after all get_specs_dir calls - Improve json_escape to handle all JSON control characters - Add explanatory comment for pre-formatted JSON array usage - Separate shared context loading into dedicated step in clarify.md
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 13 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Both create-new-feature scripts now auto-create SPECS_DIR/_shared/ with a README.md (from .specify/templates/_shared/README.md) when the shared directory does not yet exist. The README documents what files to place there, which commands consume them, and provides usage examples.
…IR is set - Source common.sh in create-new-feature.sh (fixes missing get_specs_dir/json_escape) - Skip git checkout -b and git fetch --all --prune when SPECIFY_SPECS_DIR is set - Fall back to local directory scan for feature numbering in worktree mode - Skip branch naming validation in check_feature_branch/Test-FeatureBranch - Add WORKTREE_MODE field to JSON output for LLM template awareness - Update specify.md to conditionally skip branch scanning steps 2a-2c
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (-not $specsDir) { | ||
| Write-Host "`n[specify] ERROR: Invalid SPECIFY_SPECS_DIR configuration. Aborting." -ForegroundColor Red | ||
| exit 1 | ||
| } |
There was a problem hiding this comment.
The if (-not $specsDir) branch is currently unreachable because Get-SpecsDir always returns a non-empty string. This makes the new "Invalid SPECIFY_SPECS_DIR configuration" error message misleading. Either add validation/$null return behavior to Get-SpecsDir, or remove this conditional and rely on New-Item/Resolve-Path failures for error handling.
| if (-not $specsDir) { | |
| Write-Host "`n[specify] ERROR: Invalid SPECIFY_SPECS_DIR configuration. Aborting." -ForegroundColor Red | |
| exit 1 | |
| } |
| ``` | ||
| specs/_shared/ | ||
| api-conventions.md | ||
| coding-standards.md | ||
| security-requirements.md | ||
| ``` |
There was a problem hiding this comment.
The example path uses specs/_shared/, but with SPECIFY_SPECS_DIR the shared directory is conceptually SPECS_DIR/_shared/ (which may not be under specs/). Consider updating the example to avoid implying it must live in specs/.
| if [[ -n "${SPECIFY_SPECS_DIR:-}" ]]; then | ||
| specs_dir="$SPECIFY_SPECS_DIR" | ||
| # Resolve relative paths against repo root | ||
| if [[ "$specs_dir" != /* ]]; then | ||
| specs_dir="$repo_root/$specs_dir" | ||
| fi |
There was a problem hiding this comment.
SPECIFY_SPECS_DIR is user-controlled and can now influence values emitted by get_feature_paths (which are later consumed via eval $(get_feature_paths) in multiple scripts). Because get_feature_paths wraps values in single quotes, a SPECIFY_SPECS_DIR containing a ' can break out of quoting and lead to command injection. Consider switching away from eval-based exports, or ensure values are safely shell-escaped (e.g., escape single quotes) before being embedded in the get_feature_paths output.
| get_specs_dir() { | ||
| local repo_root="${1:-$(get_repo_root)}" | ||
| local specs_dir | ||
|
|
||
| if [[ -n "${SPECIFY_SPECS_DIR:-}" ]]; then | ||
| specs_dir="$SPECIFY_SPECS_DIR" | ||
| # Resolve relative paths against repo root | ||
| if [[ "$specs_dir" != /* ]]; then | ||
| specs_dir="$repo_root/$specs_dir" | ||
| fi | ||
| else | ||
| specs_dir="$repo_root/specs" | ||
| fi | ||
|
|
||
| echo "$specs_dir" | ||
| } |
There was a problem hiding this comment.
get_specs_dir is used as though it can fail (SPECS_DIR="$(get_specs_dir ...)" || exit 1), but the function always exits 0 and just echoes a path. Either remove the || exit 1 patterns, or add validation so get_specs_dir returns non-zero for invalid configurations (e.g., empty repo_root, path exists but is not a directory, or directory cannot be created when required).
| if ($env:SPECIFY_SPECS_DIR) { | ||
| $specsDir = $env:SPECIFY_SPECS_DIR | ||
| # Resolve relative paths against repo root | ||
| if (-not [System.IO.Path]::IsPathRooted($specsDir)) { | ||
| $specsDir = Join-Path $RepoRoot $specsDir | ||
| } | ||
| return $specsDir | ||
| } | ||
| return Join-Path $RepoRoot "specs" |
There was a problem hiding this comment.
Get-SpecsDir always returns a (possibly relative-resolved) string, so callers' if (-not $specsDir) { exit 1 } checks can never trigger. Either implement real validation here (and return $null for invalid values like an existing non-directory path), or remove the dead checks and adjust the error messaging accordingly.
| if ($env:SPECIFY_SPECS_DIR) { | |
| $specsDir = $env:SPECIFY_SPECS_DIR | |
| # Resolve relative paths against repo root | |
| if (-not [System.IO.Path]::IsPathRooted($specsDir)) { | |
| $specsDir = Join-Path $RepoRoot $specsDir | |
| } | |
| return $specsDir | |
| } | |
| return Join-Path $RepoRoot "specs" | |
| $specsDir = $null | |
| if ($env:SPECIFY_SPECS_DIR) { | |
| $specsDir = $env:SPECIFY_SPECS_DIR | |
| # Resolve relative paths against repo root | |
| if (-not [System.IO.Path]::IsPathRooted($specsDir)) { | |
| $specsDir = Join-Path $RepoRoot $specsDir | |
| } | |
| } else { | |
| $specsDir = Join-Path $RepoRoot "specs" | |
| } | |
| # Validate that, if the path exists, it is a directory | |
| if (Test-Path $specsDir) { | |
| if (-not (Test-Path $specsDir -PathType Container)) { | |
| Write-Error "Invalid specs directory path '$specsDir': path exists but is not a directory." | |
| return $null | |
| } | |
| } | |
| return $specsDir |
…pecify template The LLM executing the template cannot access environment variables like SPECIFY_SPECS_DIR. Remove the broken env var conditional and rely on the WORKTREE_MODE field in the script's JSON output for reporting guidance.
Summary
This PR adds support for a centralized specs directory via the
SPECIFY_SPECS_DIRenvironment variable, enabling powerful workflows for git worktrees, multi-feature development, and team collaboration.Motivation
This addresses the use case discussed in #1547 (worktree support) with a simpler, more flexible approach. Rather than embedding worktree management into git-spec, this PR decouples git-spec from specific git workflows by letting users control where specs are stored.
Key Features
1. External Specs Directory
Set
SPECIFY_SPECS_DIRto store specs outside the repository:2. Shared Project Context
A
_shared/subdirectory within the specs directory provides project-wide context that all commands automatically incorporate:3. Worktree Compatibility
Works seamlessly with git worktrees - all worktrees can share the same specs directory:
Benefits
Changes
common.sh,create-new-feature.sh,check-prerequisites.shget_specs_dir(), update references, addSPECS_DIRto JSONcommon.ps1,create-new-feature.ps1,check-prerequisites.ps1Get-SpecsDir, update references, addSPECS_DIRto JSON_shared/loading to context stepTotal: ~50 lines across 13 files
Testing
Tested scenarios:
SPECIFY_SPECS_DIRset: Correctly uses external directorySPECS_DIRfor command templatesRelationship to #1547
This is an alternative approach to #1547's embedded worktree support. Instead of git-spec managing worktrees, this lets users manage git however they want while git-spec focuses on spec management. The key insight from #1547's discussion was correct: "rip out branch management and let the user do it how they want."