Skip to content

feat: Add SPECIFY_SPECS_DIR for centralized specs directory and worktree support#1579

Open
alanmeadows wants to merge 6 commits intogithub:mainfrom
alanmeadows:feature/centralized-specs-dir
Open

feat: Add SPECIFY_SPECS_DIR for centralized specs directory and worktree support#1579
alanmeadows wants to merge 6 commits intogithub:mainfrom
alanmeadows:feature/centralized-specs-dir

Conversation

@alanmeadows
Copy link

Summary

This PR adds support for a centralized specs directory via the SPECIFY_SPECS_DIR environment 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_DIR to store specs outside the repository:

export SPECIFY_SPECS_DIR="/path/to/myrepo.specs"
export SPECIFY_FEATURE="my-feature"

2. Shared Project Context

A _shared/ subdirectory within the specs directory provides project-wide context that all commands automatically incorporate:

myrepo.specs/
    _shared/                    # Project-wide standards
        architecture.md
        api-conventions.md
        coding-standards.md
    001-feature-a/
        spec.md
        plan.md
    002-feature-b/
        spec.md

3. Worktree Compatibility

Works seamlessly with git worktrees - all worktrees can share the same specs directory:

# In any worktree:
export SPECIFY_SPECS_DIR="${HOME}/clones/myrepo.specs"
# All git-spec commands use the centralized specs

Benefits

  • Cross-feature visibility: See all feature specs when working on any feature
  • Spec survival: Specs persist when worktrees are deleted
  • Spec-first development: Create specs before branches exist
  • Shared standards: Project-wide guidance incorporated into all commands
  • 100% backward compatible: No changes when env var is not set

Changes

Component Files Changes
Bash scripts common.sh, create-new-feature.sh, check-prerequisites.sh Add get_specs_dir(), update references, add SPECS_DIR to JSON
PowerShell scripts common.ps1, create-new-feature.ps1, check-prerequisites.ps1 Add Get-SpecsDir, update references, add SPECS_DIR to JSON
Command templates 7 files Add _shared/ loading to context step

Total: ~50 lines across 13 files

Testing

Tested scenarios:

  • Default behavior (no env vars): Works exactly as before
  • With SPECIFY_SPECS_DIR set: Correctly uses external directory
  • JSON output includes SPECS_DIR for command templates
  • Feature auto-numbering works with external specs directory

Relationship 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."

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.
Copilot AI review requested due to automatic review settings February 5, 2026 20:53
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-SpecsDir functions to bash and PowerShell common libraries to support external specs directories via SPECIFY_SPECS_DIR
  • Update all scripts to use the new specs directory functions instead of hardcoded paths
  • Add SPECS_DIR to 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
@alanmeadows
Copy link
Author

All review feedback has been addressed in the latest commit:

Path Validation (common.sh, common.ps1):

  • Added validation requiring absolute paths (must start with / on Unix, uses IsPathRooted on Windows)
  • Blocks path traversal (rejects paths containing '..')
  • Returns clear error messages if validation fails

JSON Escaping (common.sh):

  • Added json_escape() helper function that escapes backslashes, quotes, newlines, carriage returns, and tabs
  • Applied to all printf JSON outputs in check-prerequisites.sh and create-new-feature.sh

SPECS_DIR in JSON Output (create-new-feature.sh, create-new-feature.ps1):

  • Added SPECS_DIR to JSON output alongside BRANCH_NAME, SPEC_FILE, FEATURE_NUM

Template Wording (specify.md):

  • Clarified wording about where SPECS_DIR is provided

- 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
Copilot AI review requested due to automatic review settings February 5, 2026 21:45
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Alan Meadows added 2 commits February 5, 2026 14:22
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
Copilot AI review requested due to automatic review settings February 6, 2026 19:56
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +158 to +161
if (-not $specsDir) {
Write-Host "`n[specify] ERROR: Invalid SPECIFY_SPECS_DIR configuration. Aborting." -ForegroundColor Red
exit 1
}
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
if (-not $specsDir) {
Write-Host "`n[specify] ERROR: Invalid SPECIFY_SPECS_DIR configuration. Aborting." -ForegroundColor Red
exit 1
}

Copilot uses AI. Check for mistakes.
Comment on lines +28 to +33
```
specs/_shared/
api-conventions.md
coding-standards.md
security-requirements.md
```
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/.

Copilot uses AI. Check for mistakes.
Comment on lines +37 to +42
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
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +33 to +48
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"
}
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +22 to +30
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"
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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

Copilot uses AI. Check for mistakes.
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant