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 7 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.
…anch operations - Add --no-branch CLI flag to create-new-feature.sh and .ps1 - Auto-enable --no-branch when SPECIFY_SPECS_DIR env var is set - Output NO_BRANCH field in JSON (replaces WORKTREE_MODE) - Update specify.md to parse --no-branch from user arguments and skip branch scanning steps (git fetch, branch/dir lookups) - Users invoke with: /speckit.specify --no-branch "feature description"
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated 7 comments.
Comments suppressed due to low confidence (1)
scripts/powershell/create-new-feature.ps1:29
- The
-Helpoutput is out of sync with the new-NoBranchparameter: the Usage line doesn’t mention-NoBranch, and the examples don’t show how to use it. Update the help text (and fix the extra indentation beforeWrite-Host " -NoBranch...") so users can discover the flag.
Write-Host "Usage: ./create-new-feature.ps1 [-Json] [-ShortName <name>] [-Number N] <feature description>"
Write-Host ""
Write-Host "Options:"
Write-Host " -Json Output in JSON format"
Write-Host " -ShortName <name> Provide a custom short name (2-4 words) for the branch"
Write-Host " -Number N Specify branch number manually (overrides auto-detection)"
Write-Host " -NoBranch Skip branch creation (auto-enabled when SPECIFY_SPECS_DIR is set)"
Write-Host " -Help Show this help message"
Write-Host ""
Write-Host "Examples:"
Write-Host " ./create-new-feature.ps1 'Add user authentication system' -ShortName 'user-auth'"
Write-Host " ./create-new-feature.ps1 'Implement OAuth2 integration for API'"
exit 0
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| 4. **Load feature context**: Read from FEATURE_DIR: | ||
| - spec.md: Feature requirements and scope | ||
| - plan.md (if exists): Technical details, dependencies | ||
| - tasks.md (if exists): Implementation tasks | ||
| - **IF `SPECS_DIR/_shared/` exists**: Read all `.md` files for project-wide standards (security requirements, accessibility standards, coding conventions). Incorporate these into checklist generation to ensure project-wide requirements are validated. | ||
|
|
There was a problem hiding this comment.
Step 1 only instructs parsing FEATURE_DIR/AVAILABLE_DOCS, but step 4 now depends on SPECS_DIR to load shared standards from _shared/. Consider explicitly adding SPECS_DIR to the parsed JSON fields (or explaining how to derive it from FEATURE_DIR) so shared-context loading is reliable.
| 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) guard is currently unreachable because Get-SpecsDir (in common.ps1) always returns a non-empty string (either SPECIFY_SPECS_DIR resolved or the default Join-Path $RepoRoot 'specs'). Either remove this check, or add real validation to Get-SpecsDir (e.g., return $null when the path is invalid/uncreatable) so the error message can actually trigger.
| if (-not $specsDir) { | |
| Write-Host "`n[specify] ERROR: Invalid SPECIFY_SPECS_DIR configuration. Aborting." -ForegroundColor Red | |
| exit 1 | |
| } |
| 1. **Setup**: Run `{SCRIPT}` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot"). | ||
|
|
||
| 2. **Load design documents**: Read from FEATURE_DIR: | ||
| - **Required**: plan.md (tech stack, libraries, structure), spec.md (user stories with priorities) | ||
| - **Optional**: data-model.md (entities), contracts/ (API endpoints), research.md (decisions), quickstart.md (test scenarios) | ||
| - **IF `SPECS_DIR/_shared/` exists**: Read all `.md` files for project-wide context (coding standards, conventions). Use this to inform task structure and ensure alignment with established patterns. | ||
| - Note: Not all projects have all documents. Generate tasks based on what's available. |
There was a problem hiding this comment.
This outline says to parse only FEATURE_DIR and AVAILABLE_DOCS from the script output, but the next bullet relies on SPECS_DIR to locate _shared/. Since check-prerequisites now emits SPECS_DIR, explicitly include it in the “parse” step (or change the shared-context instruction to derive it from FEATURE_DIR) so the workflow is internally consistent.
| **From shared context (if available):** | ||
|
|
||
| - **IF `SPECS_DIR/_shared/` exists**: Read all `.md` files for project-wide standards (architecture decisions, coding conventions, security requirements). Use these as additional validation criteria alongside the constitution. |
There was a problem hiding this comment.
This adds a shared-context section keyed off SPECS_DIR/_shared, but earlier instructions only say to parse FEATURE_DIR and AVAILABLE_DOCS. Since the script output now includes SPECS_DIR, explicitly include it in the parsed fields (or describe deriving it from FEATURE_DIR) so the shared standards can actually be located.
| 1. **Setup**: Run `{SCRIPT}` from repo root and parse JSON for FEATURE_SPEC, IMPL_PLAN, SPECS_DIR, BRANCH. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot"). | ||
|
|
||
| 2. **Load context**: Read FEATURE_SPEC and `/memory/constitution.md`. Load IMPL_PLAN template (already copied). | ||
| 2. **Load context**: Read FEATURE_SPEC and `/memory/constitution.md`. Load IMPL_PLAN template (already copied). **IF `SPECS_DIR/_shared/` exists**: Read all `.md` files from it for project-wide context (architecture decisions, API conventions, coding standards). Use this to inform technical decisions and ensure alignment with established patterns. |
There was a problem hiding this comment.
This template assumes SPECS_DIR from {SCRIPT} points at the specs root so SPECS_DIR/_shared can be read. But the plan setup scripts (scripts/bash/setup-plan.sh and scripts/powershell/setup-plan.ps1) currently emit SPECS_DIR as the feature directory ($FEATURE_DIR), so this would look for _shared inside the feature folder and never find the shared context. Either update the setup-plan scripts to include the real specs root (and ideally a separate FEATURE_DIR field), or adjust the instruction here to derive the shared dir from the feature dir (e.g., use the parent directory of FEATURE_DIR/SPECS_DIR).
| 1. Run `{SCRIPT}` from repo root **once** (combined `--json --paths-only` mode / `-Json -PathsOnly`). Parse minimal JSON payload fields: | ||
| - `FEATURE_DIR` | ||
| - `FEATURE_SPEC` | ||
| - (Optionally capture `IMPL_PLAN`, `TASKS` for future chained flows.) | ||
| - If JSON parsing fails, abort and instruct user to re-run `/speckit.specify` or verify feature branch environment. | ||
| - For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot"). | ||
|
|
||
| 2. Load the current spec file. Perform a structured ambiguity & coverage scan using this taxonomy. For each category, mark status: Clear / Partial / Missing. Produce an internal coverage map used for prioritization (do not output raw map unless no questions will be asked). | ||
| 2. Load the current spec file from `FEATURE_SPEC`. | ||
|
|
||
| 3. Load shared project context. **IF `SPECS_DIR/_shared/` exists** (SPECS_DIR is in the JSON output): Read all `.md` files from it for project-wide context (architecture decisions, conventions, standards). Use this shared context to validate spec alignment with project standards and inform ambiguity detection in the next step. If the directory does not exist, proceed without shared context. | ||
|
|
There was a problem hiding this comment.
Step 3 requires SPECS_DIR (to load SPECS_DIR/_shared), but step 1’s “Parse minimal JSON payload fields” list omits SPECS_DIR. Since the script now outputs it, include SPECS_DIR in the fields to parse/capture so the shared-context step is actionable.
| - **IF EXISTS**: Read contracts/ for API specifications and test requirements | ||
| - **IF EXISTS**: Read research.md for technical decisions and constraints | ||
| - **IF EXISTS**: Read quickstart.md for integration scenarios | ||
| - **IF `SPECS_DIR/_shared/` exists**: Read all `.md` files for project-wide context (coding standards, API conventions, security requirements). Use this to guide implementation decisions and ensure alignment with established patterns. |
There was a problem hiding this comment.
The setup step here only mentions parsing FEATURE_DIR and AVAILABLE_DOCS, but this new bullet requires SPECS_DIR for _shared/. To avoid the agent missing the field, update the setup/parsing instructions to capture SPECS_DIR from the script JSON (or derive it from FEATURE_DIR).
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."