Skip to content

Commit d66aa26

Browse files
fsilvaortizclaude
andcommitted
fix(scripts): validate --number flag against existing branches/specs
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) <noreply@anthropic.com>
1 parent cb16412 commit d66aa26

3 files changed

Lines changed: 93 additions & 4 deletions

File tree

scripts/bash/create-new-feature.sh

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@ if [ "$USE_TIMESTAMP" = true ]; then
251251
else
252252
# Determine branch number
253253
if [ -z "$BRANCH_NUMBER" ]; then
254+
# No manual number provided -- auto-detect
254255
if [ "$HAS_GIT" = true ]; then
255256
# Check existing branches on remotes
256257
BRANCH_NUMBER=$(check_existing_branches "$SPECS_DIR")
@@ -259,6 +260,49 @@ else
259260
HIGHEST=$(get_highest_from_specs "$SPECS_DIR")
260261
BRANCH_NUMBER=$((HIGHEST + 1))
261262
fi
263+
elif [ "$ALLOW_EXISTING" = false ]; then
264+
# Manual number provided -- validate it is not already in use
265+
# (skip when --allow-existing-branch, as the caller intentionally targets an existing branch)
266+
MANUAL_NUM=$((10#$BRANCH_NUMBER))
267+
MANUAL_NUM_PADDED=$(printf "%03d" "$MANUAL_NUM")
268+
NUMBER_IN_USE=false
269+
270+
# Check specs directory for collision
271+
if [ -d "$SPECS_DIR" ]; then
272+
for dir in "$SPECS_DIR"/*/; do
273+
[ -d "$dir" ] || continue
274+
dirname=$(basename "$dir")
275+
if echo "$dirname" | grep -q "^${MANUAL_NUM_PADDED}-"; then
276+
NUMBER_IN_USE=true
277+
break
278+
fi
279+
done
280+
fi
281+
282+
# Check git branches for collision (fetch first to catch remote-only branches)
283+
if [ "$NUMBER_IN_USE" = false ] && [ "$HAS_GIT" = true ]; then
284+
git fetch --all --prune 2>/dev/null || true
285+
branches=$(git branch -a 2>/dev/null || echo "")
286+
if [ -n "$branches" ]; then
287+
while IFS= read -r branch; do
288+
clean_branch=$(echo "$branch" | sed 's/^[* ]*//; s|^remotes/[^/]*/||')
289+
if echo "$clean_branch" | grep -q "^${MANUAL_NUM_PADDED}-"; then
290+
NUMBER_IN_USE=true
291+
break
292+
fi
293+
done <<< "$branches"
294+
fi
295+
fi
296+
297+
if [ "$NUMBER_IN_USE" = true ]; then
298+
>&2 echo "Warning: --number $BRANCH_NUMBER conflicts with existing branch/spec (${MANUAL_NUM_PADDED}-*). Auto-detecting next available number."
299+
if [ "$HAS_GIT" = true ]; then
300+
BRANCH_NUMBER=$(check_existing_branches "$SPECS_DIR")
301+
else
302+
HIGHEST=$(get_highest_from_specs "$SPECS_DIR")
303+
BRANCH_NUMBER=$((HIGHEST + 1))
304+
fi
305+
fi
262306
fi
263307

264308
# Force base-10 interpretation to prevent octal conversion (e.g., 010 → 8 in octal, but should be 10 in decimal)

scripts/powershell/create-new-feature.ps1

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,13 +203,57 @@ if ($Timestamp) {
203203
} else {
204204
# Determine branch number
205205
if ($Number -eq 0) {
206+
# No manual number provided -- auto-detect
206207
if ($hasGit) {
207208
# Check existing branches on remotes
208209
$Number = Get-NextBranchNumber -SpecsDir $specsDir
209210
} else {
210211
# Fall back to local directory check
211212
$Number = (Get-HighestNumberFromSpecs -SpecsDir $specsDir) + 1
212213
}
214+
} elseif (-not $AllowExistingBranch) {
215+
# Manual number provided -- validate it is not already in use
216+
# (skip when -AllowExistingBranch, as the caller intentionally targets an existing branch)
217+
$manualNumPadded = '{0:000}' -f $Number
218+
$numberInUse = $false
219+
220+
# Check specs directory for collision
221+
if (Test-Path $specsDir) {
222+
foreach ($dir in (Get-ChildItem -Path $specsDir -Directory)) {
223+
if ($dir.Name -match "^$manualNumPadded-") {
224+
$numberInUse = $true
225+
break
226+
}
227+
}
228+
}
229+
230+
# Check git branches for collision (fetch first to catch remote-only branches)
231+
if (-not $numberInUse -and $hasGit) {
232+
try {
233+
git fetch --all --prune 2>$null | Out-Null
234+
$branches = git branch -a 2>$null
235+
if ($LASTEXITCODE -eq 0 -and $branches) {
236+
foreach ($branch in $branches) {
237+
$cleanBranch = $branch.Trim() -replace '^\*?\s*', '' -replace '^remotes/[^/]+/', ''
238+
if ($cleanBranch -match "^$manualNumPadded-") {
239+
$numberInUse = $true
240+
break
241+
}
242+
}
243+
}
244+
} catch {
245+
Write-Verbose "Could not check Git branches: $_"
246+
}
247+
}
248+
249+
if ($numberInUse) {
250+
Write-Warning "-Number $Number conflicts with existing branch/spec ($manualNumPadded-*). Auto-detecting next available number."
251+
if ($hasGit) {
252+
$Number = Get-NextBranchNumber -SpecsDir $specsDir
253+
} else {
254+
$Number = (Get-HighestNumberFromSpecs -SpecsDir $specsDir) + 1
255+
}
256+
}
213257
}
214258

215259
$featureNum = ('{0:000}' -f $Number)

tests/test_timestamp_branches.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -326,8 +326,8 @@ def test_allow_existing_creates_spec_dir(self, git_repo: Path):
326326
assert (git_repo / "specs" / "006-spec-dir").is_dir()
327327
assert (git_repo / "specs" / "006-spec-dir" / "spec.md").exists()
328328

329-
def test_without_flag_still_errors(self, git_repo: Path):
330-
"""T009: Verify backwards compatibility (error without flag)."""
329+
def test_without_flag_auto_detects_on_collision(self, git_repo: Path):
330+
"""T009: Without --allow-existing-branch, a conflicting --number auto-detects next available."""
331331
subprocess.run(
332332
["git", "checkout", "-b", "007-no-flag"],
333333
cwd=git_repo, check=True, capture_output=True,
@@ -339,8 +339,9 @@ def test_without_flag_still_errors(self, git_repo: Path):
339339
result = run_script(
340340
git_repo, "--short-name", "no-flag", "--number", "7", "No flag feature",
341341
)
342-
assert result.returncode != 0, "should fail without --allow-existing-branch"
343-
assert "already exists" in result.stderr
342+
assert result.returncode == 0, result.stderr
343+
assert "conflicts with existing branch/spec" in result.stderr
344+
assert "008-no-flag" in result.stdout
344345

345346
def test_allow_existing_no_overwrite_spec(self, git_repo: Path):
346347
"""T010: Pre-create spec.md with content, verify it is preserved."""

0 commit comments

Comments
 (0)