Skip to content

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

Open
alanmeadows wants to merge 7 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 7 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.
Alan Meadows added 2 commits February 6, 2026 12:41
…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"
Copilot AI review requested due to automatic review settings February 6, 2026 22:16
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 7 comments.

Comments suppressed due to low confidence (1)

scripts/powershell/create-new-feature.ps1:29

  • The -Help output is out of sync with the new -NoBranch parameter: 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 before Write-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.

Comment on lines 81 to 86
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.

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.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +161 to +164
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) 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.

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 27 to 33
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.
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.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +70 to +72
**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.
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.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 29 to +31
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.
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.

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

Copilot uses AI. Check for mistakes.
Comment on lines +28 to +38
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.

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.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 55 to +58
- **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.
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 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).

Copilot uses AI. Check for mistakes.
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