Skip to content

feat: Add git workflow configuration to specify init#1483

Draft
brianluby wants to merge 11 commits intogithub:mainfrom
brianluby:001-git-worktrees
Draft

feat: Add git workflow configuration to specify init#1483
brianluby wants to merge 11 commits intogithub:mainfrom
brianluby:001-git-worktrees

Conversation

@brianluby
Copy link

Summary

Add the ability to configure git worktree mode during project initialization via specify init, allowing users to develop multiple features simultaneously in parallel directories.

New CLI Options

Option Values Default Description
--git-mode branch / worktree branch Git workflow mode
--worktree-strategy sibling / nested / custom sibling Worktree location
--worktree-path absolute path - Custom path (if strategy is custom)

Key Features

  • Interactive selection for git workflow when options not provided
  • Git 2.5+ version check before allowing worktree mode
  • Conflict detection for --no-git + --git-mode worktree
  • Worktree location preview before confirmation
  • Auto-add .worktrees/ to .gitignore for nested strategy
  • Prominent agent notification when worktree mode is used (via specify.md template)
  • Naming convention: <repo>-<branch> for sibling/custom strategies

Usage Examples

# Interactive mode
specify init my-project --ai claude

# Explicit worktree with sibling strategy
specify init my-project --git-mode worktree --worktree-strategy sibling

# Worktree with nested strategy
specify init my-project --git-mode worktree --worktree-strategy nested

Test Plan

  • Test specify init with --git-mode worktree --worktree-strategy sibling
  • Test specify init with --git-mode worktree --worktree-strategy nested
  • Verify .gitignore updated for nested strategy
  • Test conflict detection: --no-git --git-mode worktree
  • Test with Git < 2.5 (should error gracefully)
  • Run /speckit.specify and verify worktree created with correct naming
  • Verify agent notification appears in worktree mode

AI Disclosure

This PR was developed with assistance from Claude Code (Claude Opus 4.5). Changes were tested and reviewed by the contributor.

🤖 Generated with Claude Code

brianluby and others added 3 commits January 15, 2026 17:54
Enable developers to work on multiple features simultaneously using git
worktrees instead of switching branches. Features can be created in
separate working directories, allowing independent development contexts.

Key changes:
- Add configure-worktree.sh/ps1 scripts for mode and strategy configuration
- Extend create-new-feature scripts with worktree creation and fallback logic
- Add read_config_value/Get-ConfigValue functions to common scripts
- Extend JSON output with FEATURE_ROOT and MODE fields for AI agent context
- Support nested, sibling, and custom worktree placement strategies
- Add graceful fallback to branch mode on worktree creation failure
- Detect and warn about uncommitted changes and orphaned worktrees

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add .specify/config.json to .gitignore to prevent committing local
  preferences with absolute paths
- Add warning when jq is not available and config file will be
  overwritten with only worktree settings
- Fix brittle JSON parsing in common.sh to support booleans and numbers
- Add nullglob handling for empty specs directory in get_highest_from_specs
- Improve fallback warning messages to clarify context switch when
  worktree creation fails and branch mode is used
- Sync updated scripts to .specify/scripts/bash/

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add the ability to configure git worktree mode during project
initialization, allowing users to develop multiple features in
parallel directories.

New CLI options:
- --git-mode (branch/worktree)
- --worktree-strategy (sibling/nested/custom)
- --worktree-path (for custom strategy)

Improvements:
- Interactive selection for git workflow when options not provided
- Git 2.5+ version check for worktree support
- Conflict detection for --no-git + --git-mode worktree
- Worktree location preview before confirmation
- Auto-add .worktrees/ to .gitignore for nested strategy
- Prominent agent notification when worktree mode is used

Worktree naming convention updated to <repo>-<branch> for
sibling and custom strategies for better clarity.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings January 17, 2026 15:46
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 comprehensive git worktree support to Spec Kit, enabling users to develop multiple features simultaneously in parallel directories instead of switching branches in a single working copy. The implementation allows project initialization with worktree configuration via new CLI options.

Changes:

  • Added CLI options for git workflow mode selection (branch vs worktree) with three worktree strategies (sibling, nested, custom)
  • Implemented git version checking to ensure Git 2.5+ for worktree support
  • Created configuration persistence in .specify/config.json with merge support
  • Updated shell scripts (bash and PowerShell) to support worktree creation with automatic fallback to branch mode
  • Modified agent instruction templates to notify about worktree directory switching requirements

Reviewed changes

Copilot reviewed 13 out of 14 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/specify_cli/init.py Added CLI options, git version validation, interactive selection, config writing, and user notifications for worktree workflow
templates/commands/specify.md Added instructions for agents to handle worktree mode and display directory switching warnings
scripts/bash/create-new-feature.sh Implemented worktree path calculation and creation logic with fallback to branch mode
scripts/powershell/create-new-feature.ps1 PowerShell equivalent of bash worktree implementation
scripts/bash/configure-worktree.sh New script for post-init worktree configuration changes
scripts/powershell/configure-worktree.ps1 PowerShell equivalent of configure-worktree script
scripts/bash/common.sh Added read_config_value function for config file parsing
scripts/powershell/common.ps1 Added Get-ConfigValue function for config file parsing
.specify/scripts/bash/* Template copies of bash scripts for distribution
WORKTREE_DESIGN.md Design documentation for worktree feature architecture
specs/001-git-worktrees/CHANGELOG.md Comprehensive feature changelog and usage documentation
.gitignore Added exclusions for config.json and .worktrees/ directory

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 1466 to 1470
worktree_notice = Panel(
f"[bold]Git Worktree Mode Enabled[/bold]\n\n"
f"When you run [cyan]/speckit.specify[/cyan], each feature will be created in its own directory {location_desc}.\n\n"
f"[yellow]Important:[/yellow] After creating a feature, you must switch your coding agent/IDE to the new worktree directory to continue working on that feature.\n\n"
f"To change this later, run: [cyan].specify/scripts/bash/configure-worktree.sh --show[/cyan]",
Copy link

Copilot AI Jan 17, 2026

Choose a reason for hiding this comment

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

The worktree notice at line 1470 hardcodes the bash script path ".specify/scripts/bash/configure-worktree.sh", but users who selected PowerShell as their script type (via --script ps or interactive selection) would need to use ".specify/scripts/powershell/configure-worktree.ps1" instead. The notice should use the selected_script variable to show the appropriate script path for the user's chosen script type.

Suggested change
worktree_notice = Panel(
f"[bold]Git Worktree Mode Enabled[/bold]\n\n"
f"When you run [cyan]/speckit.specify[/cyan], each feature will be created in its own directory {location_desc}.\n\n"
f"[yellow]Important:[/yellow] After creating a feature, you must switch your coding agent/IDE to the new worktree directory to continue working on that feature.\n\n"
f"To change this later, run: [cyan].specify/scripts/bash/configure-worktree.sh --show[/cyan]",
configure_worktree_script_map = {
"bash": ".specify/scripts/bash/configure-worktree.sh",
"ps": ".specify/scripts/powershell/configure-worktree.ps1",
}
configure_worktree_script = configure_worktree_script_map.get(
selected_script,
configure_worktree_script_map["bash"],
)
worktree_notice = Panel(
f"[bold]Git Worktree Mode Enabled[/bold]\n\n"
f"When you run [cyan]/speckit.specify[/cyan], each feature will be created in its own directory {location_desc}.\n\n"
f"[yellow]Important:[/yellow] After creating a feature, you must switch your coding agent/IDE to the new worktree directory to continue working on that feature.\n\n"
f"To change this later, run: [cyan]{configure_worktree_script} --show[/cyan]",

Copilot uses AI. Check for mistakes.
Comment on lines 1376 to 1381
# Ensure we start on a new line
if gitignore_path.exists():
with open(gitignore_path, 'r', encoding='utf-8') as rf:
content = rf.read()
if content and not content.endswith('\n'):
f.write('\n')
Copy link

Copilot AI Jan 17, 2026

Choose a reason for hiding this comment

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

There's a duplicate file read operation inside the gitignore update block. Lines 1377-1378 open and read the file to check if content ends with a newline, but this same file was just read at lines 1366-1367. The content from the first read should be reused to avoid redundant I/O operations.

Copilot uses AI. Check for mistakes.

# Git workflow mode selection
selected_git_mode = "branch" # Default
selected_worktree_strategy = "sibling" # Default
Copy link

Copilot AI Jan 17, 2026

Choose a reason for hiding this comment

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

The worktree strategy default value inconsistency could lead to confusion. In the Python CLI at line 1161, the default is set to "sibling", but in multiple other locations (bash/PowerShell scripts at lines with read_config_value calls, and in WORKTREE_DESIGN.md at line 22), the default is "nested". This inconsistency means the behavior will differ depending on whether a user explicitly sets the strategy during init or relies on defaults in the scripts.

Suggested change
selected_worktree_strategy = "sibling" # Default
selected_worktree_strategy = "nested" # Default

Copilot uses AI. Check for mistakes.
brianluby and others added 2 commits January 17, 2026 08:01
- Use selected_script to show correct configure-worktree script path
  (bash vs PowerShell) in worktree mode notice
- Remove duplicate file read in gitignore update logic
- Standardize default worktree strategy to "sibling" across all files
  (CLI, bash scripts, PowerShell scripts, design doc)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Previous commit incorrectly replaced all occurrences of "nested" with
"sibling". This fix restores the configure-worktree scripts and only
updates the default values shown when displaying configuration.

"nested" remains a valid strategy option alongside "sibling" and "custom".

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings January 17, 2026 16:39
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 14 changed files in this pull request and generated 15 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +186 to +188
sibling)
# Sibling uses repo_name-branch_name for clarity
echo "$(dirname "$repo_root")/${repo_name}-${branch_name}"
Copy link

Copilot AI Jan 17, 2026

Choose a reason for hiding this comment

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

The naming convention for custom strategy is inconsistent with the Python init command. The init command uses repo_name-branch_name format (e.g., "/tmp/worktrees/spec-kit-001-user-auth"), but this calculate_worktree_path function uses just branch_name. This will cause worktrees to be created in different locations than what was previewed during init.

Copilot uses AI. Check for mistakes.
else
echo "Current configuration ($CONFIG_FILE):"
echo " git_mode: $(read_config_value "git_mode" "branch")"
echo " worktree_strategy: $(read_config_value "worktree_strategy" "sibling")"
Copy link

Copilot AI Jan 17, 2026

Choose a reason for hiding this comment

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

The default value for worktree_strategy in the scripts is "nested", but the init command and documentation state the default is "sibling". This creates an inconsistency where the --show command will display "nested" as default instead of "sibling".

Copilot uses AI. Check for mistakes.
local strategy
local custom_path

strategy=$(read_config_value "worktree_strategy" "nested")
Copy link

Copilot AI Jan 17, 2026

Choose a reason for hiding this comment

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

The default value for worktree_strategy should be "sibling" to match the constants and init command default, not "nested". This inconsistency could cause unexpected behavior where users select "sibling" during init but the script defaults to "nested".

Copilot uses AI. Check for mistakes.
if [[ ! -f "$CONFIG_FILE" ]]; then
echo "No configuration file found. Using defaults:"
echo " git_mode: branch"
echo " worktree_strategy: sibling"
Copy link

Copilot AI Jan 17, 2026

Choose a reason for hiding this comment

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

The default value for worktree_strategy in the scripts is "nested", but the init command and documentation state the default is "sibling". This creates an inconsistency where the --show command will display "nested" as default instead of "sibling".

Copilot uses AI. Check for mistakes.
Comment on lines 174 to 196

strategy=$(read_config_value "worktree_strategy" "nested")
custom_path=$(read_config_value "worktree_custom_path" "")

case "$strategy" in
nested)
echo "$repo_root/.worktrees/$branch_name"
;;
sibling)
# Sibling to the repository
echo "$(dirname "$repo_root")/$branch_name"
;;
custom)
if [[ -n "$custom_path" ]]; then
echo "$custom_path/$branch_name"
else
# Fallback to nested if custom path not set
echo "$repo_root/.worktrees/$branch_name"
fi
;;
*)
# Default to nested
echo "$repo_root/.worktrees/$branch_name"
Copy link

Copilot AI Jan 17, 2026

Choose a reason for hiding this comment

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

The naming convention for custom strategy is inconsistent with the Python init command. The init command uses repo_name-branch_name format (e.g., "/tmp/worktrees/spec-kit-001-user-auth"), but this calculate_worktree_path function uses just branch_name. This will cause worktrees to be created in different locations than what was previewed during init.

Suggested change
strategy=$(read_config_value "worktree_strategy" "nested")
custom_path=$(read_config_value "worktree_custom_path" "")
case "$strategy" in
nested)
echo "$repo_root/.worktrees/$branch_name"
;;
sibling)
# Sibling to the repository
echo "$(dirname "$repo_root")/$branch_name"
;;
custom)
if [[ -n "$custom_path" ]]; then
echo "$custom_path/$branch_name"
else
# Fallback to nested if custom path not set
echo "$repo_root/.worktrees/$branch_name"
fi
;;
*)
# Default to nested
echo "$repo_root/.worktrees/$branch_name"
local repo_name
local worktree_dir_name
strategy=$(read_config_value "worktree_strategy" "nested")
custom_path=$(read_config_value "worktree_custom_path" "")
# Use same naming convention as Python init: repo_name-branch_name
repo_name="${repo_root##*/}"
worktree_dir_name="${repo_name}-${branch_name}"
case "$strategy" in
nested)
echo "$repo_root/.worktrees/$worktree_dir_name"
;;
sibling)
# Sibling to the repository
echo "$(dirname "$repo_root")/$worktree_dir_name"
;;
custom)
if [[ -n "$custom_path" ]]; then
echo "$custom_path/$worktree_dir_name"
else
# Fallback to nested if custom path not set
echo "$repo_root/.worktrees/$worktree_dir_name"
fi
;;
*)
# Default to nested
echo "$repo_root/.worktrees/$worktree_dir_name"

Copilot uses AI. Check for mistakes.
Comment on lines +182 to +184
nested)
# Nested uses just branch name since it's inside the repo
echo "$repo_root/.worktrees/$branch_name"
Copy link

Copilot AI Jan 17, 2026

Choose a reason for hiding this comment

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

The naming convention for sibling strategy is inconsistent with the Python init command. The init command uses repo_name-branch_name format (e.g., "spec-kit-001-user-auth"), but this calculate_worktree_path function uses just branch_name for sibling strategy. This will cause worktrees to be created in different locations than what was previewed during init.

Copilot uses AI. Check for mistakes.

# Read existing values
CURRENT_MODE=$(read_config_value "git_mode" "branch")
CURRENT_STRATEGY=$(read_config_value "worktree_strategy" "sibling")
Copy link

Copilot AI Jan 17, 2026

Choose a reason for hiding this comment

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

The default value for worktree_strategy in the scripts is "nested", but the init command and documentation state the default is "sibling". This creates an inconsistency where the --show command will display "nested" as default instead of "sibling".

Copilot uses AI. Check for mistakes.
Comment on lines 104 to 109
echo " worktree_strategy: nested"
echo " worktree_custom_path: (none)"
else
echo "Current configuration ($CONFIG_FILE):"
echo " git_mode: $(read_config_value "git_mode" "branch")"
echo " worktree_strategy: $(read_config_value "worktree_strategy" "nested")"
Copy link

Copilot AI Jan 17, 2026

Choose a reason for hiding this comment

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

The default value for worktree_strategy in the scripts is "nested", but the init command and documentation state the default is "sibling". This creates an inconsistency where the --show command will display "nested" as default instead of "sibling".

Suggested change
echo " worktree_strategy: nested"
echo " worktree_custom_path: (none)"
else
echo "Current configuration ($CONFIG_FILE):"
echo " git_mode: $(read_config_value "git_mode" "branch")"
echo " worktree_strategy: $(read_config_value "worktree_strategy" "nested")"
echo " worktree_strategy: sibling"
echo " worktree_custom_path: (none)"
else
echo "Current configuration ($CONFIG_FILE):"
echo " git_mode: $(read_config_value "git_mode" "branch")"
echo " worktree_strategy: $(read_config_value "worktree_strategy" "sibling")"

Copilot uses AI. Check for mistakes.

# Read existing values
CURRENT_MODE=$(read_config_value "git_mode" "branch")
CURRENT_STRATEGY=$(read_config_value "worktree_strategy" "nested")
Copy link

Copilot AI Jan 17, 2026

Choose a reason for hiding this comment

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

The default value for worktree_strategy in the scripts is "nested", but the init command and documentation state the default is "sibling". This creates an inconsistency where the --show command will display "nested" as default instead of "sibling".

Copilot uses AI. Check for mistakes.

echo "Configuration updated:"
echo " git_mode: $(read_config_value "git_mode" "branch")"
echo " worktree_strategy: $(read_config_value "worktree_strategy" "nested")"
Copy link

Copilot AI Jan 17, 2026

Choose a reason for hiding this comment

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

The default value for worktree_strategy in the scripts is "nested", but the init command and documentation state the default is "sibling". This creates an inconsistency where the --show command will display "nested" as default instead of "sibling".

Copilot uses AI. Check for mistakes.
- Update .specify/scripts/bash/configure-worktree.sh with sibling as
  default strategy (matching scripts/bash/ and Python init command)
- Update .specify/scripts/bash/create-new-feature.sh with:
  - sibling as default strategy
  - repo_name-branch_name naming convention for sibling/custom strategies
- Ensures installed scripts match source templates

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@brianluby brianluby marked this pull request as draft January 17, 2026 17:46
…dling

- Gate pre-flight warnings (uncommitted changes, orphaned worktrees) on
  worktree mode only — branch-mode users no longer see irrelevant warnings
- Replace silent worktree-to-branch fallbacks with clear, actionable error
  messages that guide users toward resolution
- Fix jq injection vulnerability in configure-worktree.sh by using --arg
  for all user-supplied values
- Fix PowerShell temp file leak in writability checks with try/finally
- Add optional config file path to read_config_value to avoid redundant
  get_repo_root() subprocess calls
- Restore HAS_GIT in PowerShell JSON output and add to bash for
  cross-platform consistency
- Move WORKTREE_DESIGN.md into specs/001-git-worktrees/
- Bump version to 0.0.23 with CHANGELOG entry

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings February 5, 2026 16:48
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 15 out of 16 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 +360 to +414
if [ "$GIT_MODE" = "worktree" ]; then
# Worktree mode
WORKTREE_PATH=$(calculate_worktree_path "$BRANCH_NAME" "$REPO_ROOT")
WORKTREE_PARENT=$(dirname "$WORKTREE_PATH")

# Check if parent path is writable (T029)
if [[ ! -d "$WORKTREE_PARENT" ]]; then
mkdir -p "$WORKTREE_PARENT" 2>/dev/null || {
>&2 echo "[specify] Error: Cannot create worktree parent directory: $WORKTREE_PARENT"
>&2 echo "[specify] Suggestions:"
>&2 echo "[specify] - Use nested strategy: configure-worktree.sh --strategy nested"
>&2 echo "[specify] - Switch to branch mode: configure-worktree.sh --mode branch"
>&2 echo "[specify] - Create the directory manually and retry"
exit 1
}
elif [[ ! -w "$WORKTREE_PARENT" ]]; then
>&2 echo "[specify] Error: Worktree parent directory is not writable: $WORKTREE_PARENT"
>&2 echo "[specify] Suggestions:"
>&2 echo "[specify] - Use nested strategy: configure-worktree.sh --strategy nested"
>&2 echo "[specify] - Switch to branch mode: configure-worktree.sh --mode branch"
>&2 echo "[specify] - Fix directory permissions and retry"
exit 1
fi
fi

if [ "$GIT_MODE" = "worktree" ]; then
# Check if branch already exists
if branch_exists "$BRANCH_NAME"; then
# Attach worktree to existing branch (without -b flag)
if git worktree add "$WORKTREE_PATH" "$BRANCH_NAME" 2>/dev/null; then
CREATION_MODE="worktree"
FEATURE_ROOT="$WORKTREE_PATH"
else
>&2 echo "[specify] Error: Failed to create worktree for existing branch '$BRANCH_NAME' at $WORKTREE_PATH"
>&2 echo "[specify] Suggestions:"
>&2 echo "[specify] - Check existing worktrees: git worktree list"
>&2 echo "[specify] - Remove stale worktree: git worktree remove <path>"
>&2 echo "[specify] - Prune orphaned entries: git worktree prune"
>&2 echo "[specify] - Switch to branch mode: configure-worktree.sh --mode branch"
exit 1
fi
else
# Create new branch with worktree
if git worktree add "$WORKTREE_PATH" -b "$BRANCH_NAME" 2>/dev/null; then
CREATION_MODE="worktree"
FEATURE_ROOT="$WORKTREE_PATH"
else
>&2 echo "[specify] Error: Failed to create worktree for new branch '$BRANCH_NAME' at $WORKTREE_PATH"
>&2 echo "[specify] Suggestions:"
>&2 echo "[specify] - Check existing worktrees: git worktree list"
>&2 echo "[specify] - Prune orphaned entries: git worktree prune"
>&2 echo "[specify] - Switch to branch mode: configure-worktree.sh --mode branch"
exit 1
fi
fi
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

The worktree creation logic has a duplicated conditional check. Lines 360-383 check if [ "$GIT_MODE" = "worktree" ] to set up the parent directory, and then lines 385-414 check the same condition again for the actual worktree creation. These checks should be combined into a single conditional block to improve code clarity and avoid the redundant condition evaluation.

Copilot uses AI. Check for mistakes.
Comment on lines +332 to +415
if ($gitMode -eq "worktree") {
# Worktree mode
$worktreePath = Get-WorktreePath -BranchName $branchName -RepoRoot $repoRoot
$worktreeParent = Split-Path $worktreePath -Parent

# Check if parent path is writable (T033)
if (-not (Test-Path $worktreeParent)) {
try {
New-Item -ItemType Directory -Path $worktreeParent -Force -ErrorAction Stop | Out-Null
}
catch {
Write-Error "[specify] Error: Cannot create worktree parent directory: $worktreeParent"
Write-Error "[specify] Suggestions:"
Write-Error "[specify] - Use nested strategy: configure-worktree.ps1 -Strategy nested"
Write-Error "[specify] - Switch to branch mode: configure-worktree.ps1 -Mode branch"
Write-Error "[specify] - Create the directory manually and retry"
exit 1
}
}
else {
# Test writability by attempting to create a temp file
$testFile = Join-Path $worktreeParent ".specify-write-test-$(Get-Random)"
try {
New-Item -ItemType File -Path $testFile -Force -ErrorAction Stop | Out-Null
}
catch {
Write-Error "[specify] Error: Worktree parent directory is not writable: $worktreeParent"
Write-Error "[specify] Suggestions:"
Write-Error "[specify] - Use nested strategy: configure-worktree.ps1 -Strategy nested"
Write-Error "[specify] - Switch to branch mode: configure-worktree.ps1 -Mode branch"
Write-Error "[specify] - Fix directory permissions and retry"
exit 1
}
finally {
Remove-Item $testFile -Force -ErrorAction SilentlyContinue
}
}
}
} else {

if ($gitMode -eq "worktree") {
# Check if branch already exists
if (Test-BranchExists -BranchName $branchName) {
# Attach worktree to existing branch (without -b flag)
try {
git worktree add $worktreePath $branchName 2>$null | Out-Null
if ($LASTEXITCODE -eq 0) {
$creationMode = "worktree"
$featureRoot = $worktreePath
}
else {
throw "Worktree creation failed"
}
}
catch {
Write-Error "[specify] Error: Failed to create worktree for existing branch '$branchName' at $worktreePath"
Write-Error "[specify] Suggestions:"
Write-Error "[specify] - Check existing worktrees: git worktree list"
Write-Error "[specify] - Remove stale worktree: git worktree remove <path>"
Write-Error "[specify] - Prune orphaned entries: git worktree prune"
Write-Error "[specify] - Switch to branch mode: configure-worktree.ps1 -Mode branch"
exit 1
}
}
else {
# Create new branch with worktree
try {
git worktree add $worktreePath -b $branchName 2>$null | Out-Null
if ($LASTEXITCODE -eq 0) {
$creationMode = "worktree"
$featureRoot = $worktreePath
}
else {
throw "Worktree creation failed"
}
}
catch {
Write-Error "[specify] Error: Failed to create worktree for new branch '$branchName' at $worktreePath"
Write-Error "[specify] Suggestions:"
Write-Error "[specify] - Check existing worktrees: git worktree list"
Write-Error "[specify] - Prune orphaned entries: git worktree prune"
Write-Error "[specify] - Switch to branch mode: configure-worktree.ps1 -Mode branch"
exit 1
}
}
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

Similar to the bash script, the worktree creation logic has a duplicated conditional check. Lines 332-369 check if ($gitMode -eq "worktree") to set up the parent directory, and then lines 371-415 check the same condition again for the actual worktree creation. These checks should be combined into a single conditional block to improve code clarity and avoid the redundant condition evaluation.

Copilot uses AI. Check for mistakes.
CHANGELOG.md Outdated
- **Git Worktree Support**: `create-new-feature` scripts now support worktree-based feature isolation as an alternative to branch switching
- Configure with `configure-worktree.sh` / `configure-worktree.ps1` to choose between `branch` (default) and `worktree` modes
- Three worktree placement strategies: `nested` (inside repo), `sibling` (alongside repo), `custom` (user-specified path)
- Automatic fallback to branch mode when worktree creation fails (e.g., unwritable paths)
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

The CHANGELOG states "Automatic fallback to branch mode when worktree creation fails" which is inaccurate. Looking at the code in scripts/bash/create-new-feature.sh lines 385-414 and scripts/powershell/create-new-feature.ps1 lines 371-415, when worktree creation fails, the scripts exit with error code 1 and display error messages. There is no automatic fallback to branch mode. The error handling explicitly calls exit 1 after displaying suggestions.

Suggested change
- Automatic fallback to branch mode when worktree creation fails (e.g., unwritable paths)
- Clear, actionable error messages and non-zero exit when worktree creation fails (no automatic fallback to branch mode)

Copilot uses AI. Check for mistakes.
* Example: `scripts/bash/configure-worktree.sh --mode worktree --location sibling`

## 5. Rollback Plan
* If `git worktree add` fails (e.g., path permission denied), fall back to standard branch creation with a warning. No newline at end of file
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

The design document describes a rollback plan that mentions "fall back to standard branch creation with a warning" if worktree creation fails. However, the actual implementation in the scripts exits with an error instead of falling back. This is inconsistent with the design document. Either the design document should be updated to reflect the actual behavior (exit on error), or the implementation should include a fallback mechanism as originally designed.

Copilot uses AI. Check for mistakes.
CHANGELOG.md Outdated
Comment on lines 10 to 31
## [0.0.23] - 2025-02-04

### Added

- **Git Worktree Support**: `create-new-feature` scripts now support worktree-based feature isolation as an alternative to branch switching
- Configure with `configure-worktree.sh` / `configure-worktree.ps1` to choose between `branch` (default) and `worktree` modes
- Three worktree placement strategies: `nested` (inside repo), `sibling` (alongside repo), `custom` (user-specified path)
- Automatic fallback to branch mode when worktree creation fails (e.g., unwritable paths)
- Pre-flight warnings for uncommitted changes and orphaned worktrees (worktree mode only)
- JSON output includes `FEATURE_ROOT` and `MODE` fields for automation

### Changed

- Worktree configuration stored in `.specify/config.json` (supports `git_mode`, `worktree_strategy`, `worktree_custom_path`)
- Worktree creation failures now exit with clear, actionable error messages instead of silently falling back to branch mode
- `HAS_GIT` field added to bash JSON output and restored in PowerShell JSON output for backward compatibility
- `read_config_value` now accepts an optional config file path parameter to avoid redundant repository root lookups
- Fixed jq injection vulnerability in `configure-worktree.sh` by using `--arg` for user input
- Fixed PowerShell temp file leak in writability checks with proper `try/finally` cleanup
- Moved `WORKTREE_DESIGN.md` into `specs/001-git-worktrees/` to keep design docs with their spec

## [0.0.22] - 2025-11-07
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

The changelog dates are not in chronological order. Version 0.0.23 is dated "2025-02-04", which is earlier than version 0.0.22 dated "2025-11-07". Since 0.0.23 is the newer version, it should have a more recent date. Additionally, given that the current date is February 5, 2026, the date "2025-02-04" for version 0.0.23 appears to be a typo and should likely be "2026-02-04" or "2026-02-05".

Copilot uses AI. Check for mistakes.
brianluby and others added 2 commits February 5, 2026 09:02
….specify/

The 3 scripts under .specify/scripts/bash/ were copies of scripts/bash/
and created a maintenance burden (issue M6 from PR review). Untrack them
and broaden .gitignore from just .specify/config.json to the entire
.specify/ directory, which is generated per-project by specify init.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings February 5, 2026 17:07
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
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 12 out of 13 changed files in this pull request and generated 7 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +111 to +128
# Check if parent directory exists and is writable
$parentPath = Split-Path $Path -Parent
if (-not (Test-Path $parentPath)) {
Write-Error "Error: Parent directory does not exist: $parentPath"
exit 1
}
# Test writability by attempting to create a temp file
$testFile = Join-Path $parentPath ".specify-write-test-$(Get-Random)"
try {
New-Item -ItemType File -Path $testFile -Force -ErrorAction Stop | Out-Null
}
catch {
Write-Error "Error: Parent directory is not writable: $parentPath"
exit 1
}
finally {
Remove-Item $testFile -Force -ErrorAction SilentlyContinue
}
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

The validation checks if the parent of the custom path exists and is writable, but the custom path itself is meant to be a base directory where worktrees will be created. The validation should check if the custom path can be created (if it doesn't exist) or is writable (if it exists), rather than checking its parent.

Suggested change
# Check if parent directory exists and is writable
$parentPath = Split-Path $Path -Parent
if (-not (Test-Path $parentPath)) {
Write-Error "Error: Parent directory does not exist: $parentPath"
exit 1
}
# Test writability by attempting to create a temp file
$testFile = Join-Path $parentPath ".specify-write-test-$(Get-Random)"
try {
New-Item -ItemType File -Path $testFile -Force -ErrorAction Stop | Out-Null
}
catch {
Write-Error "Error: Parent directory is not writable: $parentPath"
exit 1
}
finally {
Remove-Item $testFile -Force -ErrorAction SilentlyContinue
}
# Validate that the custom path itself is (or can be created as) a writable directory
if (Test-Path $Path) {
$pathItem = Get-Item $Path -ErrorAction Stop
if (-not $pathItem.PSIsContainer) {
Write-Error "Error: Custom path exists but is not a directory: $Path"
exit 1
}
# Test writability by attempting to create a temp file in the custom path
$testFile = Join-Path $Path ".specify-write-test-$(Get-Random)"
try {
New-Item -ItemType File -Path $testFile -Force -ErrorAction Stop | Out-Null
}
catch {
Write-Error "Error: Custom path directory is not writable: $Path"
exit 1
}
finally {
Remove-Item $testFile -Force -ErrorAction SilentlyContinue
}
}
else {
# Attempt to create the custom path directory
try {
New-Item -ItemType Directory -Path $Path -Force -ErrorAction Stop | Out-Null
}
catch {
Write-Error "Error: Cannot create directory for custom path: $Path"
exit 1
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +444 to +445
printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s","FEATURE_ROOT":"%s","MODE":"%s","HAS_GIT":%s}\n' \
"$BRANCH_NAME" "$SPEC_FILE" "$FEATURE_NUM" "$FEATURE_ROOT" "$CREATION_MODE" "$HAS_GIT"
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

The JSON output uses printf with %s format specifiers without proper escaping. If SPEC_FILE or FEATURE_ROOT contain special characters (like quotes, backslashes, or newlines), this could produce invalid JSON. Consider using a JSON library or implementing proper escaping for string values (e.g., escaping quotes, backslashes, and control characters).

Copilot uses AI. Check for mistakes.
* **Strategies**:
* `nested`: Creates worktrees inside `<PROJECT_ROOT>/.worktrees/<branch>`. (Safest for sandboxes).
* `sibling`: Creates worktrees in `../<project-name>-<branch>`. (User preferred workflow).
* `custom`: Creates worktrees in `<custom_path>/<branch>`.
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

The documentation states that the custom strategy creates worktrees in <custom_path>/<branch>, but the actual implementation (lines 36-37) uses <custom_path>/<repo_name>-<branch_name>. The documentation should be updated to match the implementation, which uses the repo name prefix for consistency with the sibling strategy.

Suggested change
* `custom`: Creates worktrees in `<custom_path>/<branch>`.
* `custom`: Creates worktrees in `<custom_path>/<repo_name>-<branch_name>`.

Copilot uses AI. Check for mistakes.
Comment on lines +1369 to +1370
# Check if entry already exists (with or without trailing newline)
if worktree_entry in existing_content or ".worktrees" in existing_content:
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

The condition ".worktrees" in existing_content is too broad and could match unintended strings like "my-worktrees" in comments or other contexts. Consider checking for the exact pattern with word boundaries or line boundaries to avoid false positives.

Suggested change
# Check if entry already exists (with or without trailing newline)
if worktree_entry in existing_content or ".worktrees" in existing_content:
# Check if entry already exists as a dedicated ignore rule
existing_lines = [line.strip() for line in existing_content.splitlines()]
if any(line == ".worktrees" or line == ".worktrees/" for line in existing_lines):

Copilot uses AI. Check for mistakes.
Comment on lines +1223 to +1243
if not Path(worktree_path).is_absolute():
console.print(f"[red]Error:[/red] --worktree-path must be an absolute path (got: {worktree_path})")
raise typer.Exit(1)
# Validate parent exists
parent = Path(worktree_path).parent
if not parent.exists():
console.print(f"[red]Error:[/red] Parent directory does not exist: {parent}")
raise typer.Exit(1)
selected_worktree_path = worktree_path
else:
# Prompt for custom path
if sys.stdin.isatty():
console.print()
selected_worktree_path = typer.prompt("Enter custom worktree base path (absolute path)")
if not Path(selected_worktree_path).is_absolute():
console.print(f"[red]Error:[/red] Path must be absolute (got: {selected_worktree_path})")
raise typer.Exit(1)
parent = Path(selected_worktree_path).parent
if not parent.exists():
console.print(f"[red]Error:[/red] Parent directory does not exist: {parent}")
raise typer.Exit(1)
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

When checking if the parent directory exists, consider that the worktree_path itself might need to be created (not just its parent). The current validation checks if the parent of worktree_path exists, but worktree_path is meant to be a base directory where multiple worktrees will be created (e.g., /tmp/worktrees/myrepo-feature1, /tmp/worktrees/myrepo-feature2). The validation should ensure the base path can be created, not just its parent.

Suggested change
if not Path(worktree_path).is_absolute():
console.print(f"[red]Error:[/red] --worktree-path must be an absolute path (got: {worktree_path})")
raise typer.Exit(1)
# Validate parent exists
parent = Path(worktree_path).parent
if not parent.exists():
console.print(f"[red]Error:[/red] Parent directory does not exist: {parent}")
raise typer.Exit(1)
selected_worktree_path = worktree_path
else:
# Prompt for custom path
if sys.stdin.isatty():
console.print()
selected_worktree_path = typer.prompt("Enter custom worktree base path (absolute path)")
if not Path(selected_worktree_path).is_absolute():
console.print(f"[red]Error:[/red] Path must be absolute (got: {selected_worktree_path})")
raise typer.Exit(1)
parent = Path(selected_worktree_path).parent
if not parent.exists():
console.print(f"[red]Error:[/red] Parent directory does not exist: {parent}")
raise typer.Exit(1)
base_path = Path(worktree_path)
if not base_path.is_absolute():
console.print(f"[red]Error:[/red] --worktree-path must be an absolute path (got: {worktree_path})")
raise typer.Exit(1)
# Validate base path is a directory or can be created
if base_path.exists():
if not base_path.is_dir():
console.print(f"[red]Error:[/red] --worktree-path exists and is not a directory: {base_path}")
raise typer.Exit(1)
else:
parent = base_path.parent
if not parent.exists():
console.print(f"[red]Error:[/red] Parent directory does not exist: {parent}")
raise typer.Exit(1)
if not os.access(parent, os.W_OK | os.X_OK):
console.print(f"[red]Error:[/red] Cannot create worktree base directory under: {parent}")
raise typer.Exit(1)
selected_worktree_path = str(base_path)
else:
# Prompt for custom path
if sys.stdin.isatty():
console.print()
selected_worktree_path = typer.prompt("Enter custom worktree base path (absolute path)")
base_path = Path(selected_worktree_path)
if not base_path.is_absolute():
console.print(f"[red]Error:[/red] Path must be absolute (got: {selected_worktree_path})")
raise typer.Exit(1)
# Validate base path is a directory or can be created
if base_path.exists():
if not base_path.is_dir():
console.print(f"[red]Error:[/red] Path exists and is not a directory: {base_path}")
raise typer.Exit(1)
else:
parent = base_path.parent
if not parent.exists():
console.print(f"[red]Error:[/red] Parent directory does not exist: {parent}")
raise typer.Exit(1)
if not os.access(parent, os.W_OK | os.X_OK):
console.print(f"[red]Error:[/red] Cannot create worktree base directory under: {parent}")
raise typer.Exit(1)
selected_worktree_path = str(base_path)

Copilot uses AI. Check for mistakes.
Comment on lines +1223 to +1243
if not Path(worktree_path).is_absolute():
console.print(f"[red]Error:[/red] --worktree-path must be an absolute path (got: {worktree_path})")
raise typer.Exit(1)
# Validate parent exists
parent = Path(worktree_path).parent
if not parent.exists():
console.print(f"[red]Error:[/red] Parent directory does not exist: {parent}")
raise typer.Exit(1)
selected_worktree_path = worktree_path
else:
# Prompt for custom path
if sys.stdin.isatty():
console.print()
selected_worktree_path = typer.prompt("Enter custom worktree base path (absolute path)")
if not Path(selected_worktree_path).is_absolute():
console.print(f"[red]Error:[/red] Path must be absolute (got: {selected_worktree_path})")
raise typer.Exit(1)
parent = Path(selected_worktree_path).parent
if not parent.exists():
console.print(f"[red]Error:[/red] Parent directory does not exist: {parent}")
raise typer.Exit(1)
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

The same parent directory validation issue exists here. The validation checks if the parent of the user-provided path exists, but the path itself is a base directory that should be validated for writability, not just its parent.

Suggested change
if not Path(worktree_path).is_absolute():
console.print(f"[red]Error:[/red] --worktree-path must be an absolute path (got: {worktree_path})")
raise typer.Exit(1)
# Validate parent exists
parent = Path(worktree_path).parent
if not parent.exists():
console.print(f"[red]Error:[/red] Parent directory does not exist: {parent}")
raise typer.Exit(1)
selected_worktree_path = worktree_path
else:
# Prompt for custom path
if sys.stdin.isatty():
console.print()
selected_worktree_path = typer.prompt("Enter custom worktree base path (absolute path)")
if not Path(selected_worktree_path).is_absolute():
console.print(f"[red]Error:[/red] Path must be absolute (got: {selected_worktree_path})")
raise typer.Exit(1)
parent = Path(selected_worktree_path).parent
if not parent.exists():
console.print(f"[red]Error:[/red] Parent directory does not exist: {parent}")
raise typer.Exit(1)
base_path = Path(worktree_path)
if not base_path.is_absolute():
console.print(f"[red]Error:[/red] --worktree-path must be an absolute path (got: {worktree_path})")
raise typer.Exit(1)
# Validate base path exists, is a directory, and is writable
if not base_path.exists():
console.print(f"[red]Error:[/red] Worktree base directory does not exist: {base_path}")
raise typer.Exit(1)
if not base_path.is_dir():
console.print(f"[red]Error:[/red] Worktree base path is not a directory: {base_path}")
raise typer.Exit(1)
if not os.access(str(base_path), os.W_OK | os.X_OK):
console.print(f"[red]Error:[/red] Worktree base directory is not writable: {base_path}")
raise typer.Exit(1)
selected_worktree_path = str(base_path)
else:
# Prompt for custom path
if sys.stdin.isatty():
console.print()
selected_worktree_path = typer.prompt("Enter custom worktree base path (absolute path)")
base_path = Path(selected_worktree_path)
if not base_path.is_absolute():
console.print(f"[red]Error:[/red] Path must be absolute (got: {selected_worktree_path})")
raise typer.Exit(1)
# Validate base path exists, is a directory, and is writable
if not base_path.exists():
console.print(f"[red]Error:[/red] Worktree base directory does not exist: {base_path}")
raise typer.Exit(1)
if not base_path.is_dir():
console.print(f"[red]Error:[/red] Worktree base path is not a directory: {base_path}")
raise typer.Exit(1)
if not os.access(str(base_path), os.W_OK | os.X_OK):
console.print(f"[red]Error:[/red] Worktree base directory is not writable: {base_path}")
raise typer.Exit(1)
selected_worktree_path = str(base_path)

Copilot uses AI. Check for mistakes.
Comment on lines +149 to +158
# Check if path is writable (create parent if needed)
CUSTOM_PARENT=$(dirname "$CUSTOM_PATH")
if [[ ! -d "$CUSTOM_PARENT" ]]; then
echo "Error: Parent directory does not exist: $CUSTOM_PARENT" >&2
exit 1
fi
if [[ ! -w "$CUSTOM_PARENT" ]]; then
echo "Error: Parent directory is not writable: $CUSTOM_PARENT" >&2
exit 1
fi
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

The validation checks if the parent of the custom path exists and is writable, but the custom path itself is meant to be a base directory where worktrees will be created. The validation should check if the custom path can be created (if it doesn't exist) or is writable (if it exists), rather than checking its parent.

Suggested change
# Check if path is writable (create parent if needed)
CUSTOM_PARENT=$(dirname "$CUSTOM_PATH")
if [[ ! -d "$CUSTOM_PARENT" ]]; then
echo "Error: Parent directory does not exist: $CUSTOM_PARENT" >&2
exit 1
fi
if [[ ! -w "$CUSTOM_PARENT" ]]; then
echo "Error: Parent directory is not writable: $CUSTOM_PARENT" >&2
exit 1
fi
# Validate the custom path itself (if it exists) or its parent (if it does not)
if [[ -e "$CUSTOM_PATH" ]]; then
if [[ ! -d "$CUSTOM_PATH" ]]; then
echo "Error: Custom path exists but is not a directory: $CUSTOM_PATH" >&2
exit 1
fi
if [[ ! -w "$CUSTOM_PATH" ]]; then
echo "Error: Custom path directory is not writable: $CUSTOM_PATH" >&2
exit 1
fi
else
CUSTOM_PARENT=$(dirname "$CUSTOM_PATH")
if [[ ! -d "$CUSTOM_PARENT" ]]; then
echo "Error: Parent directory does not exist: $CUSTOM_PARENT" >&2
exit 1
fi
if [[ ! -w "$CUSTOM_PARENT" ]]; then
echo "Error: Parent directory is not writable: $CUSTOM_PARENT" >&2
exit 1
fi
fi

Copilot uses AI. Check for mistakes.
- Fix CHANGELOG date (2025 → 2026) and remove stale "automatic fallback"
  line that contradicts the actual error-exit behavior
- Escape backslashes and quotes in jq-fallback JSON construction to
  prevent broken output from paths with special characters
- Add -ConfigFile parameter to PowerShell Get-ConfigValue for parity
  with bash read_config_value, avoiding redundant Get-RepoRoot calls
- Fix script path in __init__.py: .specify/scripts/ → scripts/ since
  .specify/ is now gitignored

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
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