From 520bba36a35ace828bc6266f2750661585eae0a0 Mon Sep 17 00:00:00 2001 From: Ray Date: Sat, 4 Oct 2025 14:29:34 +0800 Subject: [PATCH 1/8] fix: detect and warn about spec directory mismatch Fixes #733 When users delete a branch and create a new one, the spec directory name no longer matches the branch name, causing spec-kit to fail. This fix adds automatic detection of mismatched spec directories and provides clear instructions to fix the issue using git mv. Changes: - Add check_and_fix_spec_directory_mismatch() function to common.sh - Integrate mismatch check into setup-plan.sh and check-prerequisites.sh - Handle three scenarios: single orphaned dir, multiple orphaned dirs, no dir - Provide actionable git mv commands in warning messages --- scripts/bash/check-prerequisites.sh | 5 ++ scripts/bash/common.sh | 71 +++++++++++++++++++++++++++++ scripts/bash/setup-plan.sh | 3 ++ 3 files changed, 79 insertions(+) diff --git a/scripts/bash/check-prerequisites.sh b/scripts/bash/check-prerequisites.sh index f32b6245a..14a7ba803 100644 --- a/scripts/bash/check-prerequisites.sh +++ b/scripts/bash/check-prerequisites.sh @@ -82,6 +82,11 @@ source "$SCRIPT_DIR/common.sh" eval $(get_feature_paths) check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1 +# Check if branch and spec directory are in sync (skip in paths-only mode) +if ! $PATHS_ONLY; then + check_and_fix_spec_directory_mismatch || exit 1 +fi + # If paths-only mode, output paths and exit (support JSON + paths-only combined) if $PATHS_ONLY; then if $JSON_MODE; then diff --git a/scripts/bash/common.sh b/scripts/bash/common.sh index 34e5d4bb7..15ab31b4b 100644 --- a/scripts/bash/common.sh +++ b/scripts/bash/common.sh @@ -111,3 +111,74 @@ EOF check_file() { [[ -f "$1" ]] && echo " ✓ $2" || echo " ✗ $2"; } check_dir() { [[ -d "$1" && -n $(ls -A "$1" 2>/dev/null) ]] && echo " ✓ $2" || echo " ✗ $2"; } + +# Check if current branch matches a spec directory, and offer to fix mismatches +check_and_fix_spec_directory_mismatch() { + local repo_root=$(get_repo_root) + local current_branch=$(get_current_branch) + local expected_dir="$repo_root/specs/$current_branch" + + # Skip check for non-git repos or main branch + if [[ "$current_branch" == "main" ]] || ! has_git; then + return 0 + fi + + # Skip check if branch doesn't follow feature branch naming convention + if [[ ! "$current_branch" =~ ^[0-9]{3}- ]]; then + return 0 + fi + + # If expected directory exists, all good + if [[ -d "$expected_dir" ]]; then + return 0 + fi + + # Directory doesn't exist - look for orphaned spec directories + local specs_dir="$repo_root/specs" + local orphaned_dirs=() + + if [[ -d "$specs_dir" ]]; then + for dir in "$specs_dir"/*; do + if [[ -d "$dir" ]]; then + local dirname=$(basename "$dir") + # Check if this spec dir has no matching branch + if ! git rev-parse --verify "$dirname" >/dev/null 2>&1; then + orphaned_dirs+=("$dirname") + fi + fi + done + fi + + # If we found exactly one orphaned directory, suggest renaming it + if [[ ${#orphaned_dirs[@]} -eq 1 ]]; then + local orphaned="${orphaned_dirs[0]}" + echo "" >&2 + echo "⚠️ Warning: Branch '$current_branch' has no matching spec directory" >&2 + echo " Found orphaned spec directory: specs/$orphaned" >&2 + echo " This may be from a deleted or renamed branch." >&2 + echo "" >&2 + echo " To fix this issue, run:" >&2 + echo " git mv specs/$orphaned specs/$current_branch" >&2 + echo "" >&2 + return 1 + elif [[ ${#orphaned_dirs[@]} -gt 1 ]]; then + echo "" >&2 + echo "⚠️ Warning: Branch '$current_branch' has no matching spec directory" >&2 + echo " Found multiple orphaned spec directories:" >&2 + for dir in "${orphaned_dirs[@]}"; do + echo " - specs/$dir" >&2 + done + echo "" >&2 + echo " To fix this, manually rename the correct directory:" >&2 + echo " git mv specs/ specs/$current_branch" >&2 + echo "" >&2 + return 1 + else + # No spec directory exists at all - might be a new branch + echo "" >&2 + echo "⚠️ Warning: No spec directory found for branch '$current_branch'" >&2 + echo " Run /specify to create a new feature specification." >&2 + echo "" >&2 + return 1 + fi +} diff --git a/scripts/bash/setup-plan.sh b/scripts/bash/setup-plan.sh index 654ba50d7..b06dfc9d2 100644 --- a/scripts/bash/setup-plan.sh +++ b/scripts/bash/setup-plan.sh @@ -33,6 +33,9 @@ eval $(get_feature_paths) # Check if we're on a proper feature branch (only for git repos) check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1 +# Check if branch and spec directory are in sync +check_and_fix_spec_directory_mismatch || exit 1 + # Ensure the feature directory exists mkdir -p "$FEATURE_DIR" From 08de238ff1e1a12a5604b266c8abfb3dbf984972 Mon Sep 17 00:00:00 2001 From: Ray Tien Date: Sat, 4 Oct 2025 14:39:52 +0800 Subject: [PATCH 2/8] refactor: extract feature branch pattern to reusable constant --- scripts/bash/common.sh | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/scripts/bash/common.sh b/scripts/bash/common.sh index 15ab31b4b..507c9a2e1 100644 --- a/scripts/bash/common.sh +++ b/scripts/bash/common.sh @@ -1,6 +1,9 @@ #!/usr/bin/env bash # Common functions and variables for all scripts +# Feature branch naming pattern (3 digits followed by hyphen) +readonly FEATURE_BRANCH_PATTERN='^[0-9]{3}-' + # Get repository root, with fallback for non-git repositories get_repo_root() { if git rev-parse --show-toplevel >/dev/null 2>&1; then @@ -72,7 +75,7 @@ check_feature_branch() { return 0 fi - if [[ ! "$branch" =~ ^[0-9]{3}- ]]; then + if [[ ! "$branch" =~ $FEATURE_BRANCH_PATTERN ]]; then echo "ERROR: Not on a feature branch. Current branch: $branch" >&2 echo "Feature branches should be named like: 001-feature-name" >&2 return 1 @@ -124,7 +127,7 @@ check_and_fix_spec_directory_mismatch() { fi # Skip check if branch doesn't follow feature branch naming convention - if [[ ! "$current_branch" =~ ^[0-9]{3}- ]]; then + if [[ ! "$current_branch" =~ $FEATURE_BRANCH_PATTERN ]]; then return 0 fi @@ -137,7 +140,7 @@ check_and_fix_spec_directory_mismatch() { local specs_dir="$repo_root/specs" local orphaned_dirs=() - if [[ -d "$specs_dir" ]]; then + if [[ -d "$specs_dir" ]] && has_git; then for dir in "$specs_dir"/*; do if [[ -d "$dir" ]]; then local dirname=$(basename "$dir") From df76a8db41b6c3cd74590aab48f93c3f48803510 Mon Sep 17 00:00:00 2001 From: Ray Tien Date: Sun, 2 Nov 2025 12:15:25 +0800 Subject: [PATCH 3/8] fix: improve spec mismatch detection compatibility and robustness - Add variable validation and nullglob protection - Skip symlinks and hidden directories - Optimize git branch lookups - Add comprehensive function documentation --- scripts/bash/common.sh | 49 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/scripts/bash/common.sh b/scripts/bash/common.sh index 507c9a2e1..bb602a2ff 100644 --- a/scripts/bash/common.sh +++ b/scripts/bash/common.sh @@ -40,6 +40,8 @@ get_current_branch() { for dir in "$specs_dir"/*; do if [[ -d "$dir" ]]; then local dirname=$(basename "$dir") + # Note: Cannot use FEATURE_BRANCH_PATTERN here as we need the capture group + # to extract the numeric part for comparison if [[ "$dirname" =~ ^([0-9]{3})- ]]; then local number=${BASH_REMATCH[1]} number=$((10#$number)) @@ -116,9 +118,28 @@ check_file() { [[ -f "$1" ]] && echo " ✓ $2" || echo " ✗ $2"; } check_dir() { [[ -d "$1" && -n $(ls -A "$1" 2>/dev/null) ]] && echo " ✓ $2" || echo " ✗ $2"; } # Check if current branch matches a spec directory, and offer to fix mismatches +# +# This function detects "orphaned" spec directories (directories with no matching git branch) +# and provides guidance to rename them to match the current branch. +# +# Returns: +# 0 - Success (no mismatch or check skipped) +# 1 - Mismatch detected, user action required +# +# Side effects: +# Writes warning messages and remediation instructions to stderr +# +# Dependencies: +# Requires git for orphan detection (gracefully skips if unavailable) check_and_fix_spec_directory_mismatch() { local repo_root=$(get_repo_root) local current_branch=$(get_current_branch) + + # Validate required variables + if [[ -z "$repo_root" ]] || [[ -z "$current_branch" ]]; then + return 0 # Skip check if we can't determine context + fi + local expected_dir="$repo_root/specs/$current_branch" # Skip check for non-git repos or main branch @@ -141,22 +162,40 @@ check_and_fix_spec_directory_mismatch() { local orphaned_dirs=() if [[ -d "$specs_dir" ]] && has_git; then + # Get all existing branch names for efficient lookup (Bash 3.2 compatible) + local existing_branches=$(git branch --format='%(refname:short)' 2>/dev/null | tr '\n' '|') + + # Enable nullglob to handle empty directories gracefully + local original_nullglob=$(shopt -p nullglob) + shopt -s nullglob + for dir in "$specs_dir"/*; do - if [[ -d "$dir" ]]; then + # Skip if not a directory or if it's a symlink + if [[ -d "$dir" ]] && [[ ! -L "$dir" ]]; then local dirname=$(basename "$dir") + + # Skip hidden/special directories + if [[ "$dirname" =~ ^\. ]]; then + continue + fi + # Check if this spec dir has no matching branch - if ! git rev-parse --verify "$dirname" >/dev/null 2>&1; then + # Using grep for compatibility with older bash versions + if ! echo "|${existing_branches}" | grep -q "|${dirname}|"; then orphaned_dirs+=("$dirname") fi fi done + + # Restore original nullglob setting + $original_nullglob fi # If we found exactly one orphaned directory, suggest renaming it if [[ ${#orphaned_dirs[@]} -eq 1 ]]; then local orphaned="${orphaned_dirs[0]}" echo "" >&2 - echo "⚠️ Warning: Branch '$current_branch' has no matching spec directory" >&2 + echo "WARNING: Branch '$current_branch' has no matching spec directory" >&2 echo " Found orphaned spec directory: specs/$orphaned" >&2 echo " This may be from a deleted or renamed branch." >&2 echo "" >&2 @@ -166,7 +205,7 @@ check_and_fix_spec_directory_mismatch() { return 1 elif [[ ${#orphaned_dirs[@]} -gt 1 ]]; then echo "" >&2 - echo "⚠️ Warning: Branch '$current_branch' has no matching spec directory" >&2 + echo "WARNING: Branch '$current_branch' has no matching spec directory" >&2 echo " Found multiple orphaned spec directories:" >&2 for dir in "${orphaned_dirs[@]}"; do echo " - specs/$dir" >&2 @@ -179,7 +218,7 @@ check_and_fix_spec_directory_mismatch() { else # No spec directory exists at all - might be a new branch echo "" >&2 - echo "⚠️ Warning: No spec directory found for branch '$current_branch'" >&2 + echo "WARNING: No spec directory found for branch '$current_branch'" >&2 echo " Run /specify to create a new feature specification." >&2 echo "" >&2 return 1 From d92f3d9ad802ca17952d9d25d1184b937b21e29c Mon Sep 17 00:00:00 2001 From: Ray Tien Date: Sun, 2 Nov 2025 12:22:52 +0800 Subject: [PATCH 4/8] Update scripts/bash/common.sh Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- scripts/bash/common.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/bash/common.sh b/scripts/bash/common.sh index 5bb3522c0..e556d06a3 100644 --- a/scripts/bash/common.sh +++ b/scripts/bash/common.sh @@ -205,7 +205,7 @@ check_and_fix_spec_directory_mismatch() { if [[ -d "$specs_dir" ]] && has_git; then # Get all existing branch names for efficient lookup (Bash 3.2 compatible) - local existing_branches=$(git branch --format='%(refname:short)' 2>/dev/null | tr '\n' '|') + local existing_branches=$(git for-each-ref refs/heads/ --format='%(refname:short)' 2>/dev/null | tr '\n' '|') # Enable nullglob to handle empty directories gracefully local original_nullglob=$(shopt -p nullglob) From dfc4cb36adc399174caef28a8c2694e9d95d4bf5 Mon Sep 17 00:00:00 2001 From: Ray Tien Date: Sun, 2 Nov 2025 12:23:00 +0800 Subject: [PATCH 5/8] Update scripts/bash/common.sh Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- scripts/bash/common.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/bash/common.sh b/scripts/bash/common.sh index e556d06a3..187f14d97 100644 --- a/scripts/bash/common.sh +++ b/scripts/bash/common.sh @@ -230,7 +230,7 @@ check_and_fix_spec_directory_mismatch() { done # Restore original nullglob setting - $original_nullglob + eval "$original_nullglob" fi # If we found exactly one orphaned directory, suggest renaming it From 2aea108fbc1e904bf2540a9d834f18c1008522e0 Mon Sep 17 00:00:00 2001 From: Ray Tien Date: Sun, 2 Nov 2025 12:23:06 +0800 Subject: [PATCH 6/8] Update scripts/bash/common.sh Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- scripts/bash/common.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/bash/common.sh b/scripts/bash/common.sh index 187f14d97..1560053da 100644 --- a/scripts/bash/common.sh +++ b/scripts/bash/common.sh @@ -223,7 +223,7 @@ check_and_fix_spec_directory_mismatch() { # Check if this spec dir has no matching branch # Using grep for compatibility with older bash versions - if ! echo "|${existing_branches}" | grep -q "|${dirname}|"; then + if ! echo "|${existing_branches}" | grep -qF "|${dirname}|"; then orphaned_dirs+=("$dirname") fi fi From bfcbe48c673dab5e92be4e212a6a9400645430a6 Mon Sep 17 00:00:00 2001 From: Ray Tien Date: Sun, 2 Nov 2025 12:40:51 +0800 Subject: [PATCH 7/8] Update scripts/bash/setup-plan.sh Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- scripts/bash/setup-plan.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/bash/setup-plan.sh b/scripts/bash/setup-plan.sh index d0402dc74..4ac243d29 100644 --- a/scripts/bash/setup-plan.sh +++ b/scripts/bash/setup-plan.sh @@ -34,7 +34,11 @@ eval $(get_feature_paths) check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1 # Check if branch and spec directory are in sync -check_and_fix_spec_directory_mismatch || exit 1 +check_and_fix_spec_directory_mismatch +result=$? +if [[ $result -ne 0 && $result -ne 1 ]]; then + exit 1 +fi # Ensure the feature directory exists mkdir -p "$FEATURE_DIR" From 83ef3cc3e8dd8f57312fb26bbc5edbb252616d88 Mon Sep 17 00:00:00 2001 From: Ray Tien Date: Sun, 2 Nov 2025 12:40:58 +0800 Subject: [PATCH 8/8] Update scripts/bash/common.sh Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- scripts/bash/common.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/bash/common.sh b/scripts/bash/common.sh index 1560053da..1cdde5b05 100644 --- a/scripts/bash/common.sh +++ b/scripts/bash/common.sh @@ -261,7 +261,7 @@ check_and_fix_spec_directory_mismatch() { # No spec directory exists at all - might be a new branch echo "" >&2 echo "WARNING: No spec directory found for branch '$current_branch'" >&2 - echo " Run /specify to create a new feature specification." >&2 + echo " Run scripts/bash/create-new-feature.sh to create a new feature specification." >&2 echo "" >&2 return 1 fi