From d66aa26513b9134ea102432ab8ebb7c70205fe3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Silva=20Ortiz?= Date: Tue, 31 Mar 2026 16:44:55 -0300 Subject: [PATCH 1/9] fix(scripts): validate --number flag against existing branches/specs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When an AI assistant ignores the prompt instruction to omit --number and passes a conflicting value (e.g. --number 1), the script now detects the collision and auto-detects the next available number instead of silently reusing it. This prevents branch numbering from resetting to 001 — the root cause of #1744. - Check specs directories and git branches (after fetch) for collisions - Warn on stderr and fall back to auto-detection on conflict - Skip validation when --allow-existing-branch is set (intentional reuse) - Applied symmetrically to both Bash and PowerShell scripts Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/bash/create-new-feature.sh | 44 +++++++++++++++++++++++ scripts/powershell/create-new-feature.ps1 | 44 +++++++++++++++++++++++ tests/test_timestamp_branches.py | 9 ++--- 3 files changed, 93 insertions(+), 4 deletions(-) diff --git a/scripts/bash/create-new-feature.sh b/scripts/bash/create-new-feature.sh index 54ba1dbf58..ca25250add 100644 --- a/scripts/bash/create-new-feature.sh +++ b/scripts/bash/create-new-feature.sh @@ -251,6 +251,7 @@ if [ "$USE_TIMESTAMP" = true ]; then else # Determine branch number if [ -z "$BRANCH_NUMBER" ]; then + # No manual number provided -- auto-detect if [ "$HAS_GIT" = true ]; then # Check existing branches on remotes BRANCH_NUMBER=$(check_existing_branches "$SPECS_DIR") @@ -259,6 +260,49 @@ else HIGHEST=$(get_highest_from_specs "$SPECS_DIR") BRANCH_NUMBER=$((HIGHEST + 1)) fi + elif [ "$ALLOW_EXISTING" = false ]; then + # Manual number provided -- validate it is not already in use + # (skip when --allow-existing-branch, as the caller intentionally targets an existing branch) + MANUAL_NUM=$((10#$BRANCH_NUMBER)) + MANUAL_NUM_PADDED=$(printf "%03d" "$MANUAL_NUM") + NUMBER_IN_USE=false + + # Check specs directory for collision + if [ -d "$SPECS_DIR" ]; then + for dir in "$SPECS_DIR"/*/; do + [ -d "$dir" ] || continue + dirname=$(basename "$dir") + if echo "$dirname" | grep -q "^${MANUAL_NUM_PADDED}-"; then + NUMBER_IN_USE=true + break + fi + done + fi + + # Check git branches for collision (fetch first to catch remote-only branches) + if [ "$NUMBER_IN_USE" = false ] && [ "$HAS_GIT" = true ]; then + git fetch --all --prune 2>/dev/null || true + branches=$(git branch -a 2>/dev/null || echo "") + if [ -n "$branches" ]; then + while IFS= read -r branch; do + clean_branch=$(echo "$branch" | sed 's/^[* ]*//; s|^remotes/[^/]*/||') + if echo "$clean_branch" | grep -q "^${MANUAL_NUM_PADDED}-"; then + NUMBER_IN_USE=true + break + fi + done <<< "$branches" + fi + fi + + if [ "$NUMBER_IN_USE" = true ]; then + >&2 echo "Warning: --number $BRANCH_NUMBER conflicts with existing branch/spec (${MANUAL_NUM_PADDED}-*). Auto-detecting next available number." + if [ "$HAS_GIT" = true ]; then + BRANCH_NUMBER=$(check_existing_branches "$SPECS_DIR") + else + HIGHEST=$(get_highest_from_specs "$SPECS_DIR") + BRANCH_NUMBER=$((HIGHEST + 1)) + fi + fi fi # Force base-10 interpretation to prevent octal conversion (e.g., 010 → 8 in octal, but should be 10 in decimal) diff --git a/scripts/powershell/create-new-feature.ps1 b/scripts/powershell/create-new-feature.ps1 index 3708ea2db1..1835896ccc 100644 --- a/scripts/powershell/create-new-feature.ps1 +++ b/scripts/powershell/create-new-feature.ps1 @@ -203,6 +203,7 @@ if ($Timestamp) { } else { # Determine branch number if ($Number -eq 0) { + # No manual number provided -- auto-detect if ($hasGit) { # Check existing branches on remotes $Number = Get-NextBranchNumber -SpecsDir $specsDir @@ -210,6 +211,49 @@ if ($Timestamp) { # Fall back to local directory check $Number = (Get-HighestNumberFromSpecs -SpecsDir $specsDir) + 1 } + } elseif (-not $AllowExistingBranch) { + # Manual number provided -- validate it is not already in use + # (skip when -AllowExistingBranch, as the caller intentionally targets an existing branch) + $manualNumPadded = '{0:000}' -f $Number + $numberInUse = $false + + # Check specs directory for collision + if (Test-Path $specsDir) { + foreach ($dir in (Get-ChildItem -Path $specsDir -Directory)) { + if ($dir.Name -match "^$manualNumPadded-") { + $numberInUse = $true + break + } + } + } + + # Check git branches for collision (fetch first to catch remote-only branches) + if (-not $numberInUse -and $hasGit) { + try { + git fetch --all --prune 2>$null | Out-Null + $branches = git branch -a 2>$null + if ($LASTEXITCODE -eq 0 -and $branches) { + foreach ($branch in $branches) { + $cleanBranch = $branch.Trim() -replace '^\*?\s*', '' -replace '^remotes/[^/]+/', '' + if ($cleanBranch -match "^$manualNumPadded-") { + $numberInUse = $true + break + } + } + } + } catch { + Write-Verbose "Could not check Git branches: $_" + } + } + + if ($numberInUse) { + Write-Warning "-Number $Number conflicts with existing branch/spec ($manualNumPadded-*). Auto-detecting next available number." + if ($hasGit) { + $Number = Get-NextBranchNumber -SpecsDir $specsDir + } else { + $Number = (Get-HighestNumberFromSpecs -SpecsDir $specsDir) + 1 + } + } } $featureNum = ('{0:000}' -f $Number) diff --git a/tests/test_timestamp_branches.py b/tests/test_timestamp_branches.py index 0c9eb07b46..9227eaaf6c 100644 --- a/tests/test_timestamp_branches.py +++ b/tests/test_timestamp_branches.py @@ -326,8 +326,8 @@ def test_allow_existing_creates_spec_dir(self, git_repo: Path): assert (git_repo / "specs" / "006-spec-dir").is_dir() assert (git_repo / "specs" / "006-spec-dir" / "spec.md").exists() - def test_without_flag_still_errors(self, git_repo: Path): - """T009: Verify backwards compatibility (error without flag).""" + def test_without_flag_auto_detects_on_collision(self, git_repo: Path): + """T009: Without --allow-existing-branch, a conflicting --number auto-detects next available.""" subprocess.run( ["git", "checkout", "-b", "007-no-flag"], cwd=git_repo, check=True, capture_output=True, @@ -339,8 +339,9 @@ def test_without_flag_still_errors(self, git_repo: Path): result = run_script( git_repo, "--short-name", "no-flag", "--number", "7", "No flag feature", ) - assert result.returncode != 0, "should fail without --allow-existing-branch" - assert "already exists" in result.stderr + assert result.returncode == 0, result.stderr + assert "conflicts with existing branch/spec" in result.stderr + assert "008-no-flag" in result.stdout def test_allow_existing_no_overwrite_spec(self, git_repo: Path): """T010: Pre-create spec.md with content, verify it is preserved.""" From e6022750ecab7d811efac5315332e1ca2fbe0c39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Silva=20Ortiz?= Date: Tue, 31 Mar 2026 17:28:42 -0300 Subject: [PATCH 2/9] test: add static guard for PowerShell number collision validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses Copilot review comment — verifies the PowerShell script contains the collision detection logic (manualNumPadded, git fetch, warning message, AllowExistingBranch bypass). Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/test_timestamp_branches.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/test_timestamp_branches.py b/tests/test_timestamp_branches.py index 9227eaaf6c..8af01d1516 100644 --- a/tests/test_timestamp_branches.py +++ b/tests/test_timestamp_branches.py @@ -413,3 +413,15 @@ def test_powershell_supports_allow_existing_branch_flag(self): assert "-AllowExistingBranch" in contents # Ensure the flag is referenced in script logic, not just declared assert "AllowExistingBranch" in contents.replace("-AllowExistingBranch", "") + + def test_powershell_has_number_collision_validation(self): + """Static guard: PS script validates manual -Number against existing branches/specs.""" + contents = CREATE_FEATURE_PS.read_text(encoding="utf-8") + # Must check specs directory for collision + assert "manualNumPadded" in contents + # Must check git branches for collision + assert "git fetch --all --prune" in contents + # Must warn and auto-detect on conflict + assert "conflicts with existing branch/spec" in contents + # Must skip validation when -AllowExistingBranch is set + assert "elseif (-not $AllowExistingBranch)" in contents From 2f7d8f3c496bd24b8ada55587b726cecedf1279d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Silva=20Ortiz?= Date: Tue, 31 Mar 2026 17:58:36 -0300 Subject: [PATCH 3/9] =?UTF-8?q?fix:=20address=20Copilot=20review=20?= =?UTF-8?q?=E2=80=94=20input=20validation=20and=20flexible=20test=20assert?= =?UTF-8?q?ion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Bash: reject non-numeric --number with clear error before arithmetic - PowerShell: reject negative -Number values (< 1) with clear error - Test: use regex for elseif assertion to tolerate whitespace changes Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/bash/create-new-feature.sh | 4 ++++ scripts/powershell/create-new-feature.ps1 | 4 ++++ tests/test_timestamp_branches.py | 6 ++++-- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/scripts/bash/create-new-feature.sh b/scripts/bash/create-new-feature.sh index ca25250add..f0efed9f24 100644 --- a/scripts/bash/create-new-feature.sh +++ b/scripts/bash/create-new-feature.sh @@ -263,6 +263,10 @@ else elif [ "$ALLOW_EXISTING" = false ]; then # Manual number provided -- validate it is not already in use # (skip when --allow-existing-branch, as the caller intentionally targets an existing branch) + if ! echo "$BRANCH_NUMBER" | grep -q '^[0-9]\+$'; then + >&2 echo "Error: --number requires a positive integer, got '$BRANCH_NUMBER'" + exit 1 + fi MANUAL_NUM=$((10#$BRANCH_NUMBER)) MANUAL_NUM_PADDED=$(printf "%03d" "$MANUAL_NUM") NUMBER_IN_USE=false diff --git a/scripts/powershell/create-new-feature.ps1 b/scripts/powershell/create-new-feature.ps1 index 1835896ccc..6fb1013016 100644 --- a/scripts/powershell/create-new-feature.ps1 +++ b/scripts/powershell/create-new-feature.ps1 @@ -214,6 +214,10 @@ if ($Timestamp) { } elseif (-not $AllowExistingBranch) { # Manual number provided -- validate it is not already in use # (skip when -AllowExistingBranch, as the caller intentionally targets an existing branch) + if ($Number -lt 1) { + Write-Error "-Number requires a positive integer, got $Number" + exit 1 + } $manualNumPadded = '{0:000}' -f $Number $numberInUse = $false diff --git a/tests/test_timestamp_branches.py b/tests/test_timestamp_branches.py index 8af01d1516..2fdeff6165 100644 --- a/tests/test_timestamp_branches.py +++ b/tests/test_timestamp_branches.py @@ -423,5 +423,7 @@ def test_powershell_has_number_collision_validation(self): assert "git fetch --all --prune" in contents # Must warn and auto-detect on conflict assert "conflicts with existing branch/spec" in contents - # Must skip validation when -AllowExistingBranch is set - assert "elseif (-not $AllowExistingBranch)" in contents + # Must skip validation when -AllowExistingBranch is set; allow flexible whitespace + assert re.search(r"elseif\s*\(\s*-not\s+\$AllowExistingBranch\s*\)", contents), ( + "Expected an elseif guard that negates $AllowExistingBranch" + ) From 59153f7f30379cfadb64fb8fb673f5a38cde9197 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Silva=20Ortiz?= Date: Tue, 31 Mar 2026 18:08:45 -0300 Subject: [PATCH 4/9] fix: move input validation before collision check to cover all paths - Bash: reject non-numeric and zero --number before if/elif (fixes crash with --allow-existing-branch + non-numeric --number) - PowerShell: reject negative -Number before if/elseif (same issue) - Ensures consistent behavior: 0 is rejected in Bash (PS already treats 0 as auto-detect) Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/bash/create-new-feature.sh | 12 ++++++++---- scripts/powershell/create-new-feature.ps1 | 10 ++++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/scripts/bash/create-new-feature.sh b/scripts/bash/create-new-feature.sh index f0efed9f24..8e35f9e169 100644 --- a/scripts/bash/create-new-feature.sh +++ b/scripts/bash/create-new-feature.sh @@ -249,6 +249,14 @@ if [ "$USE_TIMESTAMP" = true ]; then FEATURE_NUM=$(date +%Y%m%d-%H%M%S) BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}" else + # Validate --number input when provided + if [ -n "$BRANCH_NUMBER" ]; then + if ! echo "$BRANCH_NUMBER" | grep -q '^[0-9]\+$' || [ "$((10#$BRANCH_NUMBER))" -lt 1 ]; then + >&2 echo "Error: --number requires a positive integer, got '$BRANCH_NUMBER'" + exit 1 + fi + fi + # Determine branch number if [ -z "$BRANCH_NUMBER" ]; then # No manual number provided -- auto-detect @@ -263,10 +271,6 @@ else elif [ "$ALLOW_EXISTING" = false ]; then # Manual number provided -- validate it is not already in use # (skip when --allow-existing-branch, as the caller intentionally targets an existing branch) - if ! echo "$BRANCH_NUMBER" | grep -q '^[0-9]\+$'; then - >&2 echo "Error: --number requires a positive integer, got '$BRANCH_NUMBER'" - exit 1 - fi MANUAL_NUM=$((10#$BRANCH_NUMBER)) MANUAL_NUM_PADDED=$(printf "%03d" "$MANUAL_NUM") NUMBER_IN_USE=false diff --git a/scripts/powershell/create-new-feature.ps1 b/scripts/powershell/create-new-feature.ps1 index 6fb1013016..fbdf3b51f6 100644 --- a/scripts/powershell/create-new-feature.ps1 +++ b/scripts/powershell/create-new-feature.ps1 @@ -201,6 +201,12 @@ if ($Timestamp) { $featureNum = Get-Date -Format 'yyyyMMdd-HHmmss' $branchName = "$featureNum-$branchSuffix" } else { + # Validate -Number input when provided + if ($Number -lt 0) { + Write-Error "-Number requires a positive integer, got $Number" + exit 1 + } + # Determine branch number if ($Number -eq 0) { # No manual number provided -- auto-detect @@ -214,10 +220,6 @@ if ($Timestamp) { } elseif (-not $AllowExistingBranch) { # Manual number provided -- validate it is not already in use # (skip when -AllowExistingBranch, as the caller intentionally targets an existing branch) - if ($Number -lt 1) { - Write-Error "-Number requires a positive integer, got $Number" - exit 1 - } $manualNumPadded = '{0:000}' -f $Number $numberInUse = $false From 6cf7bd107dd81cee13797e1e5017644b79fca66a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Silva=20Ortiz?= Date: Tue, 31 Mar 2026 18:10:36 -0300 Subject: [PATCH 5/9] test: add coverage for number validation and spec-directory collision - Reject --number 0, non-numeric, and negative values - Detect collision via specs directory (not just branches) - Accept unused --number as-is - Static guard: PS script rejects negative -Number Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/test_timestamp_branches.py | 45 ++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/tests/test_timestamp_branches.py b/tests/test_timestamp_branches.py index 2fdeff6165..a4b591cd39 100644 --- a/tests/test_timestamp_branches.py +++ b/tests/test_timestamp_branches.py @@ -406,6 +406,45 @@ def test_allow_existing_no_git(self, no_git_dir: Path): assert result.returncode == 0, result.stderr +class TestNumberInputValidation: + """Tests for --number input validation and spec-directory collision detection.""" + + def test_rejects_zero(self, git_repo: Path): + result = run_script(git_repo, "--short-name", "zero", "--number", "0", "Zero test") + assert result.returncode != 0 + assert "positive integer" in result.stderr + + def test_rejects_non_numeric(self, git_repo: Path): + result = run_script(git_repo, "--short-name", "abc", "--number", "abc", "Non-numeric test") + assert result.returncode != 0 + assert "positive integer" in result.stderr + + def test_rejects_negative(self, git_repo: Path): + result = run_script(git_repo, "--short-name", "neg", "--number", "-5", "Negative test") + assert result.returncode != 0 + assert "positive integer" in result.stderr + + def test_collision_detected_via_specs_directory(self, git_repo: Path): + """Collision detected from specs dir even when no matching branch exists.""" + spec_dir = git_repo / "specs" / "003-existing-spec" + spec_dir.mkdir(parents=True) + (spec_dir / "spec.md").write_text("# Existing\n") + result = run_script( + git_repo, "--short-name", "new-feature", "--number", "3", "Spec collision test", + ) + assert result.returncode == 0 + assert "conflicts with existing branch/spec" in result.stderr + assert "003-new-feature" not in result.stdout + + def test_unused_number_accepted(self, git_repo: Path): + """A manual --number that doesn't collide is accepted as-is.""" + result = run_script( + git_repo, "--short-name", "free", "--number", "50", "Free number test", + ) + assert result.returncode == 0, result.stderr + assert "050-free" in result.stdout + + class TestAllowExistingBranchPowerShell: def test_powershell_supports_allow_existing_branch_flag(self): """Static guard: PS script exposes and uses -AllowExistingBranch.""" @@ -414,6 +453,12 @@ def test_powershell_supports_allow_existing_branch_flag(self): # Ensure the flag is referenced in script logic, not just declared assert "AllowExistingBranch" in contents.replace("-AllowExistingBranch", "") + def test_powershell_rejects_negative_number(self): + """Static guard: PS script validates -Number is not negative.""" + contents = CREATE_FEATURE_PS.read_text(encoding="utf-8") + assert "Number -lt 0" in contents or re.search(r"\$Number\s+-lt\s+0", contents) + assert "positive integer" in contents + def test_powershell_has_number_collision_validation(self): """Static guard: PS script validates manual -Number against existing branches/specs.""" contents = CREATE_FEATURE_PS.read_text(encoding="utf-8") From a8e710e7c7f12548236186d5205761d77dac69fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Silva=20Ortiz?= Date: Tue, 31 Mar 2026 18:34:22 -0300 Subject: [PATCH 6/9] fix: use PSBoundParameters to distinguish explicit -Number 0 from default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses Copilot review — previously -Number 0 silently fell through to auto-detect. Now uses $PSBoundParameters.ContainsKey('Number') to reject explicitly passed values < 1, matching Bash behavior. Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/powershell/create-new-feature.ps1 | 4 ++-- tests/test_timestamp_branches.py | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/scripts/powershell/create-new-feature.ps1 b/scripts/powershell/create-new-feature.ps1 index fbdf3b51f6..0d4fa6a055 100644 --- a/scripts/powershell/create-new-feature.ps1 +++ b/scripts/powershell/create-new-feature.ps1 @@ -201,8 +201,8 @@ if ($Timestamp) { $featureNum = Get-Date -Format 'yyyyMMdd-HHmmss' $branchName = "$featureNum-$branchSuffix" } else { - # Validate -Number input when provided - if ($Number -lt 0) { + # Validate -Number input when explicitly provided + if ($PSBoundParameters.ContainsKey('Number') -and $Number -lt 1) { Write-Error "-Number requires a positive integer, got $Number" exit 1 } diff --git a/tests/test_timestamp_branches.py b/tests/test_timestamp_branches.py index a4b591cd39..598a3b9bd4 100644 --- a/tests/test_timestamp_branches.py +++ b/tests/test_timestamp_branches.py @@ -453,10 +453,11 @@ def test_powershell_supports_allow_existing_branch_flag(self): # Ensure the flag is referenced in script logic, not just declared assert "AllowExistingBranch" in contents.replace("-AllowExistingBranch", "") - def test_powershell_rejects_negative_number(self): - """Static guard: PS script validates -Number is not negative.""" + def test_powershell_rejects_invalid_number(self): + """Static guard: PS script validates -Number using PSBoundParameters and rejects < 1.""" contents = CREATE_FEATURE_PS.read_text(encoding="utf-8") - assert "Number -lt 0" in contents or re.search(r"\$Number\s+-lt\s+0", contents) + assert "PSBoundParameters.ContainsKey" in contents + assert "Number -lt 1" in contents or re.search(r"\$Number\s+-lt\s+1", contents) assert "positive integer" in contents def test_powershell_has_number_collision_validation(self): From 135163be24213b9b735ef488b04562ee532eba25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Silva=20Ortiz?= Date: Tue, 31 Mar 2026 19:03:08 -0300 Subject: [PATCH 7/9] fix: remove redundant fetch from bash collision check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Avoids double fetch when collision is detected — check_existing_branches already fetches internally during auto-detect fallback. Matches the PowerShell script change from previous commit. Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/bash/create-new-feature.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/bash/create-new-feature.sh b/scripts/bash/create-new-feature.sh index 8e35f9e169..293789116f 100644 --- a/scripts/bash/create-new-feature.sh +++ b/scripts/bash/create-new-feature.sh @@ -287,9 +287,8 @@ else done fi - # Check git branches for collision (fetch first to catch remote-only branches) + # Check git branches for collision if [ "$NUMBER_IN_USE" = false ] && [ "$HAS_GIT" = true ]; then - git fetch --all --prune 2>/dev/null || true branches=$(git branch -a 2>/dev/null || echo "") if [ -n "$branches" ]; then while IFS= read -r branch; do From 2495abeed484f0e7ee869c9c03499d95b98c50ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Silva=20Ortiz?= Date: Tue, 31 Mar 2026 19:06:19 -0300 Subject: [PATCH 8/9] fix: remove redundant fetch from PowerShell collision check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Matches bash change from 135163b — the collision check now uses git branch -a with available refs. If a collision triggers auto-detect, Get-NextBranchNumber fetches internally. Updates static guard to match. Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/powershell/create-new-feature.ps1 | 3 +-- tests/test_timestamp_branches.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/scripts/powershell/create-new-feature.ps1 b/scripts/powershell/create-new-feature.ps1 index 0d4fa6a055..11fa1e72b3 100644 --- a/scripts/powershell/create-new-feature.ps1 +++ b/scripts/powershell/create-new-feature.ps1 @@ -233,10 +233,9 @@ if ($Timestamp) { } } - # Check git branches for collision (fetch first to catch remote-only branches) + # Check git branches for collision if (-not $numberInUse -and $hasGit) { try { - git fetch --all --prune 2>$null | Out-Null $branches = git branch -a 2>$null if ($LASTEXITCODE -eq 0 -and $branches) { foreach ($branch in $branches) { diff --git a/tests/test_timestamp_branches.py b/tests/test_timestamp_branches.py index 598a3b9bd4..0fc4e1cf46 100644 --- a/tests/test_timestamp_branches.py +++ b/tests/test_timestamp_branches.py @@ -466,7 +466,7 @@ def test_powershell_has_number_collision_validation(self): # Must check specs directory for collision assert "manualNumPadded" in contents # Must check git branches for collision - assert "git fetch --all --prune" in contents + assert "git branch -a" in contents # Must warn and auto-detect on conflict assert "conflicts with existing branch/spec" in contents # Must skip validation when -AllowExistingBranch is set; allow flexible whitespace From 29302c247ef0540ecba35ccea1325ceb80803d37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Silva=20Ortiz?= Date: Tue, 31 Mar 2026 19:21:23 -0300 Subject: [PATCH 9/9] fix: use portable grep -E and restore fetch in collision check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - grep -Eq instead of grep -q with non-POSIX \+ (portability fix) - Restore git fetch before collision check in both scripts: correctness over performance — without fetch, remote-only branches are invisible and collisions go undetected Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/bash/create-new-feature.sh | 5 +++-- scripts/powershell/create-new-feature.ps1 | 3 ++- tests/test_timestamp_branches.py | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/scripts/bash/create-new-feature.sh b/scripts/bash/create-new-feature.sh index 293789116f..075cfc4c43 100644 --- a/scripts/bash/create-new-feature.sh +++ b/scripts/bash/create-new-feature.sh @@ -251,7 +251,7 @@ if [ "$USE_TIMESTAMP" = true ]; then else # Validate --number input when provided if [ -n "$BRANCH_NUMBER" ]; then - if ! echo "$BRANCH_NUMBER" | grep -q '^[0-9]\+$' || [ "$((10#$BRANCH_NUMBER))" -lt 1 ]; then + if ! echo "$BRANCH_NUMBER" | grep -Eq '^[0-9]+$' || [ "$((10#$BRANCH_NUMBER))" -lt 1 ]; then >&2 echo "Error: --number requires a positive integer, got '$BRANCH_NUMBER'" exit 1 fi @@ -287,8 +287,9 @@ else done fi - # Check git branches for collision + # Check git branches for collision (fetch to catch remote-only branches) if [ "$NUMBER_IN_USE" = false ] && [ "$HAS_GIT" = true ]; then + git fetch --all --prune 2>/dev/null || true branches=$(git branch -a 2>/dev/null || echo "") if [ -n "$branches" ]; then while IFS= read -r branch; do diff --git a/scripts/powershell/create-new-feature.ps1 b/scripts/powershell/create-new-feature.ps1 index 11fa1e72b3..8dee67ef41 100644 --- a/scripts/powershell/create-new-feature.ps1 +++ b/scripts/powershell/create-new-feature.ps1 @@ -233,9 +233,10 @@ if ($Timestamp) { } } - # Check git branches for collision + # Check git branches for collision (fetch to catch remote-only branches) if (-not $numberInUse -and $hasGit) { try { + git fetch --all --prune 2>$null | Out-Null $branches = git branch -a 2>$null if ($LASTEXITCODE -eq 0 -and $branches) { foreach ($branch in $branches) { diff --git a/tests/test_timestamp_branches.py b/tests/test_timestamp_branches.py index 0fc4e1cf46..598a3b9bd4 100644 --- a/tests/test_timestamp_branches.py +++ b/tests/test_timestamp_branches.py @@ -466,7 +466,7 @@ def test_powershell_has_number_collision_validation(self): # Must check specs directory for collision assert "manualNumPadded" in contents # Must check git branches for collision - assert "git branch -a" in contents + assert "git fetch --all --prune" in contents # Must warn and auto-detect on conflict assert "conflicts with existing branch/spec" in contents # Must skip validation when -AllowExistingBranch is set; allow flexible whitespace