Skip to content

Conversation

@askpatrickw
Copy link

Fixes #945, #61

Implements three source management modes (branch, worktree, none) with automatic Git environment detection during project initialization.

Key Changes:

  • Add Git environment detection with auto-mode suggestion
    • Detects bare repos, worktrees, normal repos, and non-Git environments
    • Suggests appropriate mode: worktree for bare/worktree contexts, branch for normal repos, none when Git unavailable
  • Implement configuration management system
    • New config file: .specify/memory/config.json
    • Schema validation with version "1.0"
    • Conditional field validation (worktree_folder required for worktree mode)
    • Backward compatibility: defaults to branch mode if config missing
  • Update bash scripts for mode-aware workflows
    • create-new-feature.sh: Creates worktrees or branches based on config
    • common.sh: Add worktree validation helpers
    • PowerShell equivalents updated for Windows support
  • Add mode indicators across all commands
    • specify check displays current mode (Branch/Worktree/None)
    • Graceful degradation for invalid configs
  • Comprehensive documentation updates
    • README: New "Source Management Modes" section with decision matrix

Testing:

All 10 test scenarios passed including branch workflows, worktree detection, none mode operation, backward compatibility, invalid config handling, and mode indicator consistency.

Specification:

  • All 51 tasks completed across 8 phases
  • Contracts: detection-api.md, config-api.md
  • Full backward compatibility maintained

Co-authored-by: Spec-Driven Development Process

Implements three source management modes (branch, worktree, none) with automatic Git environment detection during project initialization.

**Key Changes:**

- Add Git environment detection with auto-mode suggestion
  - Detects bare repos, worktrees, normal repos, and non-Git environments
  - Suggests appropriate mode: worktree for bare/worktree contexts,
    branch for normal repos, none when Git unavailable
- Implement configuration management system
  - New config file: .specify/memory/config.json
  - Schema validation with version "1.0"
  - Conditional field validation (worktree_folder required for worktree mode)
  - Backward compatibility: defaults to branch mode if config missing
- Update bash scripts for mode-aware workflows
  - create-new-feature.sh: Creates worktrees or branches based on config
  - common.sh: Add worktree validation helpers
  - PowerShell equivalents updated for Windows support
- Add mode indicators across all commands
  - `specify check` displays current mode (Branch/Worktree/None)
  - Graceful degradation for invalid configs
- Comprehensive documentation updates
  - README: New "Source Management Modes" section with decision matrix

**Testing:**

All 10 test scenarios passed including branch workflows, worktree detection, none mode operation, backward compatibility, invalid config handling, and mode indicator consistency.

**Specification:**

- All 51 tasks completed across 8 phases
- Contracts: detection-api.md, config-api.md
- Full backward compatibility maintained

Co-authored-by: Spec-Driven Development Process
Copilot AI review requested due to automatic review settings February 1, 2026 08:31
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 Git worktree support to spec-kit, introducing three source management modes (branch, worktree, none) with automatic Git environment detection. The implementation creates a configuration system that automatically selects an appropriate mode during project initialization based on the detected Git environment.

Changes:

  • Added Git environment detection logic to identify bare repositories, worktrees, and standard Git repos, with automatic mode selection
  • Implemented configuration management system with JSON schema validation stored in .specify/memory/config.json
  • Updated bash and PowerShell scripts to create worktrees or branches based on configured mode, with folder validation and permission checks
  • Added mode indicators to specify check command and enhanced documentation with workflow examples

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 16 comments.

Show a summary per file
File Description
src/specify_cli/init.py Adds Git environment detection functions, config management (load/save/validate), automatic mode selection in init command, and mode display in check command
scripts/bash/create-new-feature.sh Reads config to determine mode, creates worktrees with validation instead of branches when in worktree mode, handles none mode by skipping Git operations
scripts/powershell/create-new-feature.ps1 PowerShell equivalent of bash changes with same worktree creation and validation logic
scripts/bash/common.sh Adds worktree/branch name alignment validation for worktree directories
README.md Documents three source management modes with usage examples, configuration format, and workflow patterns
Comments suppressed due to low confidence (1)

src/specify_cli/init.py:1420

  • The config is created after template extraction but before Git initialization. If Git initialization is attempted but fails (lines 1406-1420), the config file will already exist with a mode that may no longer be appropriate. For example, if a user runs init in a normal repo, the config will be set to "branch" mode, but if Git init subsequently fails, the repo might end up in an inconsistent state with the config suggesting Git operations are available.

Consider creating the config after successful Git initialization, or add logic to clean up the config if Git initialization fails.

            # Detect Git environment and create config (Feature 001: Worktree Detection)
            # Always create config, even with --no-git (will detect "none" mode)
            original_cwd = Path.cwd()
            os.chdir(project_path)
            try:
                git_env = detect_git_environment()
                suggested_mode = git_env.suggest_mode()

                # Create config with suggested mode
                config_data = {
                    "version": "1.0",
                    "source_management_flow": suggested_mode
                }

                # Add worktree_folder if in worktree mode
                if suggested_mode == "worktree":
                    config_data["worktree_folder"] = "./worktrees"

                save_config(config_data)
            finally:
                os.chdir(original_cwd)

            if not no_git:
                tracker.start("git")
                if is_git_repo(project_path):
                    tracker.complete("git", "existing repo detected")
                elif should_init_git:
                    success, error_msg = init_git_repo(project_path, quiet=True)
                    if success:
                        tracker.complete("git", "initialized")
                    else:
                        tracker.error("git", "init failed")
                        git_error_message = error_msg
                else:
                    tracker.skip("git", "git not available")
            else:
                tracker.skip("git", "--no-git flag")

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

@askpatrickw
Copy link
Author

001-worktree-detection.zip

This is the Spec Kit output (and the constitution) used to make this feature in case that is interesting to anyone.

I used OpenCode and Claude 4.5 Sonnet.

Addresses multiple UX and error handling issues identified in code review:
1. Add mode mismatch warnings when Git unavailable
   - Scripts now warn users when config expects Git-based mode (branch/worktree)
     but Git is unavailable
   - Provides actionable guidance: what's happening and how to fix it
   - Applied to both bash and PowerShell create-new-feature scripts
2. Implement interactive source mode selection
   - Users now choose source management mode during `specify init`
   - Shows detected Git environment with context
   - Displays recommendations based on environment detection
   - Allows override via arrow-key selection interface
   - Replaces silent automatic configuration
3. Add directory recovery on script errors
   - Save original directory at script start
   - Restore original directory on any error
   - Prevents users being stranded in unexpected locations
   - Applied to all critical operations (cd, mkdir, cp, touch)
4. Add worktree path conflict detection
   - Pre-check if worktree path exists before creation
   - Provide detailed resolution options with specific commands
   - Include git worktree remove, alternative branch names, manual deletion
5. Update documentation for interactive mode selection
   - Change "automatically detects/selects" to "recommends (you can choose)"
   - Add comprehensive "Mode Selection" section with 4-step process
   - Include example selection screen
   - Clarify user agency in mode choice
6. Remove unused imports
   - Remove unused Prompt and Confirm from rich.prompt
   - Code uses select_with_arrows (custom) and typer.confirm instead
All changes maintain consistency between bash and PowerShell implementations
and preserve backward compatibility.
Addresses two additional robustness issues identified in code review:

1. Fix bare repository detection in create-new-feature scripts
   - Changed from 'git rev-parse --show-toplevel' to '--git-dir'
   - --show-toplevel fails in bare repositories, causing misclassification
   - Added bare repo check with 'git rev-parse --is-bare-repository'
   - Use git-dir as root for bare repos, show-toplevel for non-bare
   - Applied to both bash and PowerShell implementations
   - Tested and confirmed working in both bare repos and worktrees

2. Add strict config validation for worktree_folder field
   - Reject worktree_folder when mode is 'branch' or 'none'
   - Previously allowed unused field in config (confusing/misleading)
   - Now enforces: worktree_folder only valid in worktree mode
   - Provides clear error messages explaining the constraint
   - Prevents configuration inconsistencies

Both changes improve error handling and prevent edge case failures
in Git repository detection and configuration management.
The check_feature_branch() function displayed 'Warning' but returned
failure (exit code 1), creating ambiguity about whether the check was
blocking or informational.

Since Spec Kit creates all worktrees with matching directory/branch
names, any mismatch indicates user deviation from the expected workflow
(manual worktree creation, directory renaming, or wrong branch checkout).

Changes:
- Changed message from 'Warning' to 'ERROR' to match return code
- Enhanced error message with clear explanation of Spec Kit's expectation
- Provided step-by-step resolution instructions
- Maintains blocking behavior (return 1) to prevent script execution
  in misconfigured worktrees

This aligns the semantic meaning with the actual behavior and makes
it clear that worktree/branch name alignment is a requirement for
Spec Kit workflows, not just a suggestion.
Copilot AI review requested due to automatic review settings February 1, 2026 20: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 5 out of 5 changed files in this pull request and generated 11 comments.


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

Comment on lines +673 to +683
# Check 1: Git installed and in a repo?
try:
subprocess.run(
["git", "rev-parse", "--is-inside-work-tree"],
check=True,
capture_output=True,
cwd=Path.cwd(),
)
has_git = True
except (subprocess.CalledProcessError, FileNotFoundError):
return GitEnvironment(has_git=False, is_bare=False, is_worktree=False)
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

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

In bare repository detection, detect_git_environment() uses git rev-parse --is-inside-work-tree to check for Git availability. However, this command will FAIL in bare repositories (returning non-zero exit code) because bare repositories have no working tree. This means bare repositories will be incorrectly classified as "no Git" (has_git=False).

The function should first check if Git is available at all (using git rev-parse --git-dir or similar), then separately check if it's a bare repository.

Copilot uses AI. Check for mistakes.
Comment on lines +85 to +86
local worktree_dir=$(basename "$(pwd)")

Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

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

The worktree validation logic uses basename "$(pwd)" to get the directory name, but this will only work if the user is currently in the worktree directory. If they're in a subdirectory of the worktree (e.g., worktrees/001-feature/src/), this will return the subdirectory name (e.g., "src") instead of the worktree name (e.g., "001-feature"), causing a false mismatch error.

Consider using git rev-parse --show-toplevel to get the worktree root directory before extracting the basename.

Suggested change
local worktree_dir=$(basename "$(pwd)")
local worktree_root
local worktree_dir
if worktree_root="$(git rev-parse --show-toplevel 2>/dev/null)"; then
worktree_dir="$(basename "$worktree_root")"
else
# Fallback: use current directory name if git metadata is unavailable
worktree_dir="$(basename "$(pwd)")"
fi

Copilot uses AI. Check for mistakes.
Comment on lines +199 to +205
# Parse JSON config using grep/sed (no jq dependency needed)
SOURCE_MODE=$(grep -o '"source_management_flow"[[:space:]]*:[[:space:]]*"[^"]*"' "$CONFIG_FILE" | sed 's/.*"\([^"]*\)".*/\1/' || echo "branch")
WORKTREE_FOLDER=$(grep -o '"worktree_folder"[[:space:]]*:[[:space:]]*"[^"]*"' "$CONFIG_FILE" | sed 's/.*"\([^"]*\)".*/\1/' || echo "")
fi

# Fallback to branch mode if config is invalid
if [ -z "$SOURCE_MODE" ]; then
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

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

The JSON parsing using grep/sed is fragile and may fail with certain valid JSON formats. For example:

  • If there's no whitespace after the colon: "source_management_flow":"branch"
  • If there are newlines in the string value
  • If the field appears in a nested object or comment

The regex pattern [[:space:]]* requires at least the ability to match zero spaces, but the overall pattern may still fail. Consider using a more robust approach or adding validation that the parsed value is one of the expected modes (branch/worktree/none).

Suggested change
# Parse JSON config using grep/sed (no jq dependency needed)
SOURCE_MODE=$(grep -o '"source_management_flow"[[:space:]]*:[[:space:]]*"[^"]*"' "$CONFIG_FILE" | sed 's/.*"\([^"]*\)".*/\1/' || echo "branch")
WORKTREE_FOLDER=$(grep -o '"worktree_folder"[[:space:]]*:[[:space:]]*"[^"]*"' "$CONFIG_FILE" | sed 's/.*"\([^"]*\)".*/\1/' || echo "")
fi
# Fallback to branch mode if config is invalid
if [ -z "$SOURCE_MODE" ]; then
# Parse JSON config using python's json module for robustness
if CONFIG_VALUES=$(python3 - <<'PY' "$CONFIG_FILE" 2>/dev/null
import json
import sys
path = sys.argv[1]
try:
with open(path, 'r', encoding='utf-8') as f:
data = json.load(f)
except Exception:
# On any parsing/IO error, fall back to defaults handled in shell
print("branch")
print("")
sys.exit(0)
source_mode = data.get("source_management_flow", "branch")
worktree_folder = data.get("worktree_folder", "")
# Ensure we always print exactly two lines
print(source_mode if isinstance(source_mode, str) else "branch")
print(worktree_folder if isinstance(worktree_folder, str) else "")
PY
); then
SOURCE_MODE=$(printf '%s\n' "$CONFIG_VALUES" | sed -n '1p')
WORKTREE_FOLDER=$(printf '%s\n' "$CONFIG_VALUES" | sed -n '2p')
fi
fi
# Fallback to branch mode if config is invalid or unexpected
if [ -z "$SOURCE_MODE" ] || { [ "$SOURCE_MODE" != "branch" ] && [ "$SOURCE_MODE" != "worktree" ] && [ "$SOURCE_MODE" != "none" ]; }; then

Copilot uses AI. Check for mistakes.
Comment on lines +845 to +849
console.print(f"[red]Error:[/red] Invalid config file: {e}")
console.print(f"[yellow]Config location:[/yellow] {get_config_path(path)}")
console.print(
"[dim]Delete the config file and run 'specify init' to recreate it.[/dim]"
)
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

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

The get_current_mode function respects warn_if_missing for missing configs but always prints error messages for invalid configs (lines 845-849), even when warn_if_missing=False. This means the check command (which calls get_current_mode(warn_if_missing=False)) will still display config validation errors, which might be unexpected or distracting when users just want to check tool availability.

Consider adding a parameter like warn_on_error to control whether validation errors should be printed, or at least respect the warn_if_missing flag for all output.

Suggested change
console.print(f"[red]Error:[/red] Invalid config file: {e}")
console.print(f"[yellow]Config location:[/yellow] {get_config_path(path)}")
console.print(
"[dim]Delete the config file and run 'specify init' to recreate it.[/dim]"
)
if warn_if_missing:
console.print(f"[red]Error:[/red] Invalid config file: {e}")
console.print(f"[yellow]Config location:[/yellow] {get_config_path(path)}")
console.print(
"[dim]Delete the config file and run 'specify init' to recreate it.[/dim]"
)

Copilot uses AI. Check for mistakes.
Comment on lines +355 to +358
# Create worktree with new branch
git worktree add -b "$BRANCH_NAME" "$WORKTREE_PATH" || {
>&2 echo "[specify] Error: Failed to create worktree"
exit 1
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

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

When git worktree add -b fails after creating the branch but before completing the worktree setup (e.g., due to filesystem issues), the branch may be left dangling without a corresponding worktree. The error handler at line 356-358 doesn't attempt to clean up the branch in such cases.

Consider adding cleanup logic to remove the newly created branch if worktree creation fails, or at least document this potential state in the error message.

Suggested change
# Create worktree with new branch
git worktree add -b "$BRANCH_NAME" "$WORKTREE_PATH" || {
>&2 echo "[specify] Error: Failed to create worktree"
exit 1
# Track whether the branch existed before creating the worktree
if git show-ref --verify --quiet "refs/heads/$BRANCH_NAME"; then
BRANCH_ALREADY_EXISTS=true
else
BRANCH_ALREADY_EXISTS=false
fi
# Create worktree with new branch
git worktree add -b "$BRANCH_NAME" "$WORKTREE_PATH" || {
worktree_status=$?
>&2 echo "[specify] Error: Failed to create worktree at: $WORKTREE_PATH"
# If the branch did not exist before, but exists now, attempt cleanup
if [ "$BRANCH_ALREADY_EXISTS" = "false" ]; then
if git show-ref --verify --quiet "refs/heads/$BRANCH_NAME"; then
>&2 echo "[specify] Attempting to delete dangling branch '$BRANCH_NAME' created during failed worktree setup..."
if ! git branch -D "$BRANCH_NAME" >/dev/null 2>&1; then
>&2 echo "[specify] Warning: Failed to delete dangling branch '$BRANCH_NAME'."
>&2 echo "[specify] You may need to delete it manually: git branch -D \"$BRANCH_NAME\""
fi
fi
fi
exit "$worktree_status"

Copilot uses AI. Check for mistakes.
exit 1
}

TEMPLATE="$REPO_ROOT/.specify/templates/spec-template.md"
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

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

After creating a worktree and changing directory to it, REPO_ROOT is updated to the worktree path (line 371). This causes the template lookup at line 403 to search for .specify/templates/spec-template.md inside the worktree directory rather than the main repository. Since .specify/ doesn't exist in worktrees, the template will never be found, and an empty spec.md file will be created instead.

Consider preserving the original repository root in a separate variable (e.g., MAIN_REPO_ROOT) so that the template can be found regardless of the current working directory.

Suggested change
TEMPLATE="$REPO_ROOT/.specify/templates/spec-template.md"
# Use the main repository root when resolving the template path so that
# worktree mode still finds the shared .specify directory in the main repo.
if [ "$SOURCE_MODE" = "worktree" ] && [ "$HAS_GIT" = true ]; then
MAIN_REPO_ROOT="$ORIGINAL_DIR"
else
MAIN_REPO_ROOT="$REPO_ROOT"
fi
TEMPLATE="$MAIN_REPO_ROOT/.specify/templates/spec-template.md"

Copilot uses AI. Check for mistakes.
exit 1
}

$template = Join-Path $repoRoot '.specify/templates/spec-template.md'
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

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

Same issue as in the bash script: after creating a worktree and updating $repoRoot to the worktree path (line 359), the template lookup at line 402 searches for .specify/templates/spec-template.md inside the worktree directory rather than the main repository. Since .specify/ doesn't exist in worktrees, the template will never be found.

Consider preserving the original repository root in a separate variable so that the template can be found regardless of the current working directory.

Suggested change
$template = Join-Path $repoRoot '.specify/templates/spec-template.md'
# Use the main repository root for templates, even when using a worktree
if ($sourceMode -eq 'worktree' -and $hasGit) {
$templateRepoRoot = $originalDir
} else {
$templateRepoRoot = $repoRoot
}
$template = Join-Path $templateRepoRoot '.specify/templates/spec-template.md'

Copilot uses AI. Check for mistakes.
Comment on lines +391 to 395
# Update SPECS_DIR if we're in a worktree
if [ "$SOURCE_MODE" = "worktree" ] && [ "$HAS_GIT" = true ]; then
SPECS_DIR="$REPO_ROOT/specs"
fi

Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

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

This line appears to be redundant. The code only reaches this point in worktree mode if Git is available (checked at line 303), and REPO_ROOT has already been updated to WORKTREE_PATH at line 371. So this line is just setting SPECS_DIR to $WORKTREE_PATH/specs, which is already the correct value. The condition at line 392 should never be false when this code is reached in worktree mode, making this update unnecessary.

Suggested change
# Update SPECS_DIR if we're in a worktree
if [ "$SOURCE_MODE" = "worktree" ] && [ "$HAS_GIT" = true ]; then
SPECS_DIR="$REPO_ROOT/specs"
fi

Copilot uses AI. Check for mistakes.
Comment on lines +356 to +360
git worktree add -b "$BRANCH_NAME" "$WORKTREE_PATH" || {
>&2 echo "[specify] Error: Failed to create worktree"
exit 1
}

Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

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

The error message "Failed to create worktree" is too generic. If the branch name already exists, Git will return a specific error, but this handler suppresses that detail. Users won't know whether the failure is due to:

  • Branch name already exists
  • Filesystem/permission issues
  • Invalid Git state
  • Other Git errors

Consider capturing and displaying the actual Git error message to help users diagnose the problem.

Suggested change
git worktree add -b "$BRANCH_NAME" "$WORKTREE_PATH" || {
>&2 echo "[specify] Error: Failed to create worktree"
exit 1
}
if ! output=$(git worktree add -b "$BRANCH_NAME" "$WORKTREE_PATH" 2>&1); then
>&2 echo "[specify] Error: Failed to create worktree"
>&2 echo "[specify] Git reported:"
>&2 echo "$output"
exit 1
fi
# Re-emit git output on success (if any)
if [ -n "$output" ]; then
echo "$output"
fi

Copilot uses AI. Check for mistakes.
Comment on lines +1615 to +1620
# Detect Git environment and create config (Feature 001: Worktree Detection)
# Always create config, even with --no-git (will detect "none" mode)
original_cwd = Path.cwd()
os.chdir(project_path)
try:
git_env = detect_git_environment()
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

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

Git environment detection happens after template extraction (line 1620), when the script has already changed directory to the project path (line 1618). This means:

  1. If using --here in a bare repository, after template extraction, the directory will have working files and won't be detected as bare anymore
  2. The detection is based on the state AFTER initialization, not BEFORE

This could lead to incorrect mode suggestions. For example, if a user runs specify init --here in a bare repository, the template extraction adds working files, making it no longer bare. The detection will then suggest "branch" mode instead of "worktree" mode.

Consider detecting the Git environment BEFORE template extraction, or detecting it relative to the original working directory before changing to project_path.

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