Skip to content

Feature/configure branch name#1009

Open
ricardojmendez wants to merge 15 commits intogithub:mainfrom
ricardojmendez:feature/configure-branch-name
Open

Feature/configure branch name#1009
ricardojmendez wants to merge 15 commits intogithub:mainfrom
ricardojmendez:feature/configure-branch-name

Conversation

@ricardojmendez
Copy link

Contains several commits allowing the user to specify some values around branch and spec name:

  • Branch prefix, allowing for feature/, bugfix/, or other patterns to match the user's / team's approach;
  • An arbitrary spec number, to potentially tie it into the corresponding issue on Github, instead of just using a sequence.

It also includes an optional configuration file for default branch name, since I expect users will create one type of branch more often than others (eg. "feature").

This addresses some of the requests in #407.

When running 'specify', the user can now indicate a branch prefix to use,
in case they have in-house naming conventions for branches.

Examples:

/speckit.specify a feature.  Add this stuff.
/speckit.specify --branch-prefix bugfix. Solve the crash in fizzbuzz.

It also introduces a config file so that you can configure your default
branch names, since I personally end up defaulting to "feature" most of
the time.
This allows you to create branches which have a number that matches your
issue number in Github (or any other tracker).
This makes it clearer that the user may enter input such as:

- "Branch type feature, number 303" or
- "This is bugfix 31416"

It should also allow for timestamp hotfixes, given they're only a number.
Copilot AI review requested due to automatic review settings October 22, 2025 08:08
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 configuration capabilities for customizing branch naming patterns and spec numbers in the Specify CLI tool. The changes enable users to set default branch prefixes (e.g., feature/, bugfix/) and specify custom spec numbers to align with issue trackers.

Key changes:

  • Introduced .specify/config.json for project-wide branch prefix configuration
  • Added --branch-prefix and --spec-number parameters to creation scripts
  • Enhanced AI agent instructions to parse natural language patterns like "feature 303" or "bugfix 666"

Reviewed Changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
templates/config.json New configuration file template with branch prefix setting
templates/commands/specify.md Extensive documentation for parsing spec numbers and branch prefixes from user input
src/specify_cli/init.py Added setup_config_file() function to copy config template during initialization
scripts/powershell/create-new-feature.ps1 Implemented config parsing and new parameters for branch customization
scripts/bash/create-new-feature.sh Bash equivalent of PowerShell script changes
pyproject.toml Version bump to 0.0.21
README.md Documentation for new configuration features and usage examples
CHANGELOG.md Release notes for version 0.0.21

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

- "hotfix 42" → extract prefix `hotfix/` and number `42`
- "fix 123" → extract prefix `bugfix/` and number `123` (normalize "fix" to "bugfix")
- "chore 999" → extract prefix `chore/` and number `999`

Copy link

Copilot AI Oct 22, 2025

Choose a reason for hiding this comment

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

The natural language pattern documentation states that "fix" normalizes to "bugfix/" (line 29), but line 69 lists both "bugfix" and "fix" as separate recognized prefix types that map to "bugfix/". This creates ambiguity about whether "fix" is a valid pattern on its own or only when followed by a number. Consider clarifying the distinction between "fix" as a standalone prefix keyword versus its use in natural language patterns.

Suggested change
> **Note:** In natural language patterns, the prefix "fix" is always normalized to "bugfix/" for consistency (e.g., "fix 123" becomes "bugfix/123"). "fix" is not accepted as a standalone prefix; use "bugfix" for explicit prefix specification.

Copilot uses AI. Check for mistakes.
Comment on lines +204 to +206
# Use grep and sed to extract the prefix value from JSON
# This avoids requiring jq to be installed
local prefix=$(grep -o '"prefix"[[:space:]]*:[[:space:]]*"[^"]*"' "$config_file" | sed 's/.*"prefix"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')
Copy link

Copilot AI Oct 22, 2025

Choose a reason for hiding this comment

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

The JSON parsing logic fails when the prefix value contains escaped quotes or special characters. For example, a prefix value of "test\"value" would break the regex pattern. While unlikely in typical branch prefix usage, consider adding validation or a comment warning about this limitation.

Suggested change
# Use grep and sed to extract the prefix value from JSON
# This avoids requiring jq to be installed
local prefix=$(grep -o '"prefix"[[:space:]]*:[[:space:]]*"[^"]*"' "$config_file" | sed 's/.*"prefix"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')
# Use jq to extract the prefix value from JSON. jq correctly handles escaped quotes and special characters.
# If jq is not available, fall back to grep/sed but warn about limitations.
local prefix=""
if command -v jq >/dev/null 2>&1; then
prefix=$(jq -r '.prefix // empty' "$config_file")
else
echo "Warning: jq not found. Falling back to regex-based JSON parsing, which may fail for escaped quotes or special characters in the prefix value." >&2
# Fallback: regex-based extraction (does not handle escaped quotes/special chars)
prefix=$(grep -o '"prefix"[[:space:]]*:[[:space:]]*"[^"]*"' "$config_file" | sed 's/.*"prefix"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')
fi

Copilot uses AI. Check for mistakes.
if ($SpecNumber) {
# Validate it's a positive integer
$num = 0
if (-not [int]::TryParse($SpecNumber, [ref]$num) -or $num -lt 0) {
Copy link

Copilot AI Oct 22, 2025

Choose a reason for hiding this comment

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

The validation check allows zero as a valid spec number ($num -lt 0), but spec numbers starting at 0 could create ambiguous directory names like 000-feature-name. Consider changing the condition to $num -le 0 to require positive integers starting from 1.

Copilot uses AI. Check for mistakes.
Comment on lines +130 to +131
if ! [[ "$SPEC_NUM_ARG" =~ ^[0-9]+$ ]]; then
echo "Error: --spec-number must be a positive integer" >&2
Copy link

Copilot AI Oct 22, 2025

Choose a reason for hiding this comment

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

The bash validation allows zero as a valid spec number (regex ^[0-9]+$ matches "0"), but this could create ambiguous directory names like 000-feature-name. The PowerShell version has the same issue. Both scripts should reject zero to ensure spec numbers start from 1.

Suggested change
if ! [[ "$SPEC_NUM_ARG" =~ ^[0-9]+$ ]]; then
echo "Error: --spec-number must be a positive integer" >&2
if ! [[ "$SPEC_NUM_ARG" =~ ^[1-9][0-9]*$ ]]; then
echo "Error: --spec-number must be a positive integer greater than zero" >&2

Copilot uses AI. Check for mistakes.
I had missed it during the original push.
It's sometimes missing cases - for instance, if a prompt says near the
end that "This is feature 221", it may pick up on the feature number but
not the branch type.

Testing a more verbose, explicit approach.
Preserving the --number parameter from merge commit e6d6f3c, while
maintaining the new --branch-prefix parameter.
Copilot AI review requested due to automatic review settings October 31, 2025 10:33
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 10 out of 10 changed files in this pull request and generated 2 comments.


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

Comment on lines +2 to +4
"branch": {
"prefix": ""
}
Copy link

Copilot AI Oct 31, 2025

Choose a reason for hiding this comment

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

The config file structure uses "branch": { "prefix": "" } (nested structure), but the PowerShell script at line 257 reads $config.branch_prefix (flat structure with underscore). The bash script at line 250 also searches for "branch_prefix" (flat structure). This mismatch means the config file will not be read correctly. Either change the config structure to {"branch_prefix": ""} or update the scripts to read $config.branch.prefix and "branch"[^"]*"prefix".

Suggested change
"branch": {
"prefix": ""
}
"branch_prefix": ""

Copilot uses AI. Check for mistakes.
# Account for: feature number (3) + hyphen (1) = 4 chars
MAX_SUFFIX_LENGTH=$((MAX_BRANCH_LENGTH - 4))
# Account for: prefix length + feature number (3) + hyphen (1)
local prefix_length=${#BRANCH_PREFIX}
Copy link

Copilot AI Oct 31, 2025

Choose a reason for hiding this comment

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

The local keyword is used outside a function scope. This variable declaration is in the main script body (inside an if block), not within a function. Remove the local keyword or the script will fail with a syntax error.

Suggested change
local prefix_length=${#BRANCH_PREFIX}
prefix_length=${#BRANCH_PREFIX}

Copilot uses AI. Check for mistakes.
@ricardojmendez
Copy link
Author

Any feedback on this PR, @localden?

@ricardojmendez ricardojmendez marked this pull request as draft November 23, 2025 09:56
@ricardojmendez ricardojmendez marked this pull request as ready for review November 23, 2025 10:00
Copilot AI review requested due to automatic review settings November 23, 2025 10:00
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 10 out of 10 changed files in this pull request and generated 6 comments.

Comments suppressed due to low confidence (3)

scripts/bash/create-new-feature.sh:229

  • The SPECIFY_SPEC_NUMBER environment variable is documented in README.md and the specify.md template (line 103), but the bash script doesn't check or use this environment variable. The BRANCH_NUMBER variable is only set from the CLI argument (--number).

To match the documented behavior, add a check for the environment variable. For example, after parsing arguments and before the auto-increment logic:

if [ -z "$BRANCH_NUMBER" ] && [ -n "$SPECIFY_SPEC_NUMBER" ]; then
    BRANCH_NUMBER="$SPECIFY_SPEC_NUMBER"
fi
    # Common stop words to filter out
    local stop_words="^(i|a|an|the|to|for|of|in|on|at|by|with|from|is|are|was|were|be|been|being|have|has|had|do|does|did|will|would|should|could|can|may|might|must|shall|this|that|these|those|my|your|our|their|want|need|add|get|set)$"
    
    # Convert to lowercase and split into words
    local clean_name=$(echo "$description" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/ /g')
    
    # Filter words: remove stop words and words shorter than 3 chars (unless they're uppercase acronyms in original)
    local meaningful_words=()
    for word in $clean_name; do
        # Skip empty words
        [ -z "$word" ] && continue
        
        # Keep words that are NOT stop words AND (length >= 3 OR are potential acronyms)
        if ! echo "$word" | grep -qiE "$stop_words"; then
            if [ ${#word} -ge 3 ]; then
                meaningful_words+=("$word")
            elif echo "$description" | grep -q "\b${word^^}\b"; then
                # Keep short words if they appear as uppercase in original (likely acronyms)
                meaningful_words+=("$word")

scripts/powershell/create-new-feature.ps1:258

  • The configuration file structure uses a nested JSON format ("branch": { "prefix": "" }), but the PowerShell script is looking for a flat branch_prefix field. This mismatch will prevent the configuration file from working correctly.

The script should access the nested structure like: $config.branch.prefix instead of $config.branch_prefix.

    $branchSuffix = Get-BranchName -Description $featureDesc
}

scripts/bash/create-new-feature.sh:252

  • The configuration file structure uses a nested JSON format ("branch": { "prefix": "" }), but the bash script is looking for a flat branch_prefix field using grep. This mismatch will prevent the configuration file from working correctly.

The grep pattern should be updated to extract from the nested structure. For example:

local branch_prefix=$(grep -A 1 '"branch"' "$config_file" | grep '"prefix"' | sed 's/.*"prefix"[^"]*"\([^"]*\)".*/\1/')
        local cleaned=$(clean_branch_name "$description")
        echo "$cleaned" | tr '-' '\n' | grep -v '^$' | head -3 | tr '\n' '-' | sed 's/-$//'
    fi

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


**If no spec number is specified:** The script will auto-increment from the highest existing spec number (default behavior).

**Priority order:**
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

Duplicate line detected. The mapping 'fix' → 'bugfix/' appears twice (lines 108 and 109).

Copilot uses AI. Check for mistakes.
Comment on lines +59 to +225
d. Run the script `{SCRIPT}` with the calculated number and short-name:
- Pass `--number N+1` and `--short-name "your-short-name"` along with the feature description
- **CRITICAL**: Use the spec number and branch prefix **extracted in Step 1a** if available.
- Bash example: `{SCRIPT} --json --number 5 --short-name "user-auth" "Add user authentication"`
- PowerShell example: `{SCRIPT} -Json -Number 5 -ShortName "user-auth" "Add user authentication"`
- PowerShell example: `{SCRIPT} -Json -Number 5 -ShortName "user-auth" "Add user authentication"`

**IMPORTANT**:

- Check all three sources (remote branches, local branches, specs directories) to find the highest number
- Only match branches/directories with the exact short-name pattern
- If no existing branches/directories found with this short-name, start with number 1
- You must only ever run this script once per feature
- Append the short-name argument to the `{SCRIPT}` command with the 2-4 word short name you created in step 1. Keep the feature description as the final argument.
- If a spec number was extracted in Step 1a, include it as a parameter
- If a branch prefix was extracted in Step 1a, include it as a parameter
- **Note:** Natural language patterns like "feature 303" or "bugfix 666" provide BOTH prefix and number - extract and pass both parameters
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

This section contains duplicate instructions. Lines 211-217 are nearly identical to lines 219-225, repeating guidance about checking all sources, matching patterns, and extracting parameters. This duplication makes the documentation harder to maintain and could confuse users. Consider consolidating these into a single, clear set of instructions.

Copilot uses AI. Check for mistakes.
Comment on lines +32 to +33
Write-Host " SPECIFY_SPEC_NUMBER Set default spec/branch number"
Write-Host " SPECIFY_BRANCH_PREFIX Set default branch prefix"
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

The help text mentions SPECIFY_SPEC_NUMBER environment variable, but the script doesn't actually check or use this environment variable. The $Number parameter is only set from the CLI argument (-Number), not from the environment variable.

To match the documented behavior, add a check for the environment variable before defaulting to 0. For example, after the param block:

if ($Number -eq 0 -and $env:SPECIFY_SPEC_NUMBER) {
    $Number = [int]$env:SPECIFY_SPEC_NUMBER
}

Copilot uses AI. Check for mistakes.
Comment on lines 201 to 203

2. **Check for existing branches before creating new one**:

Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

Step 2 is missing sub-step 'c'. The outline jumps from step 2b (line 201-202) directly to step 2d (line 203). This makes the documentation confusing and incomplete.

Based on the removed lines in the diff context, it appears that step 2c should contain the logic for determining the next available number (extracting all numbers, finding the highest N, using N+1).

Copilot uses AI. Check for mistakes.
- "issue #221" ✓ (just number)
- "Add shopping cart feature 303" ✓ (adjacent but later in sentence)

**Examples of user input with spec number:**
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

Duplicate line detected. The example "bugfix 666 fix payment timeout" appears twice (lines 69 and 70).

Copilot uses AI. Check for mistakes.
local matches=()
if [[ -d "$specs_dir" ]]; then
for dir in "$specs_dir"/"$prefix"-*; do
for dir in "$specs_dir"/*"$number"-*; do
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

The glob pattern "$specs_dir"/*"$number"-* could match unintended directories. For example, if $number is "001", this would match directories like "foo-001-bar" or "2001-space" in addition to the intended "001-name" or "feature/001-name".

Consider using a more precise search approach similar to the PowerShell implementation that explicitly checks the directory names against the regex pattern, or improve the glob pattern to avoid false matches.

Copilot uses AI. Check for mistakes.
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 10 out of 10 changed files in this pull request and generated 8 comments.

Comments suppressed due to low confidence (2)

scripts/bash/common.sh:48

  • When spec directories are created with branch prefixes (e.g., specs/feature/001-user-auth/), this glob "$specs_dir"/* will match specs/feature/ but basename will return just feature, which won't match the pattern ^(([a-z]+/)?([0-9]{3,}))-. This function will fail to find the latest feature when prefixed directories are used.

Consider searching at depth 2 or using a different approach to handle the nested directory structure created by branch prefixes.

        for dir in "$specs_dir"/*; do
            if [[ -d "$dir" ]]; then
                local dirname=$(basename "$dir")
                # Support both formats: 001-name or feature/001-name
                if [[ "$dirname" =~ ^(([a-z]+/)?([0-9]{3,}))- ]]; then
                    local number=${BASH_REMATCH[3]}
                    number=$((10#$number))
                    if [[ "$number" -gt "$highest" ]]; then
                        highest=$number
                        latest_feature=$dirname
                    fi
                fi

scripts/powershell/common.ps1:50

  • When spec directories are created with branch prefixes (e.g., specs/feature/001-user-auth/), Get-ChildItem -Path $specsDir -Directory will return specs/feature/ but $_.Name will be just feature, which won't match the pattern ^(([a-z]+/)?(\d{3,}))-. This function will fail to find the latest feature when prefixed directories are used.

Consider using -Recurse -Depth 1 or searching in nested subdirectories to handle the directory structure created by branch prefixes.

        Get-ChildItem -Path $specsDir -Directory | ForEach-Object {
            # Support both formats: 001-name or feature/001-name
            if ($_.Name -match '^(([a-z]+/)?(\d{3,}))-') {
                $num = [int]$matches[3]
                if ($num -gt $highest) {
                    $highest = $num
                    $latestFeature = $_.Name
                }
            }

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

Comment on lines +302 to +303
if ($config.branch_prefix) {
return $config.branch_prefix
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

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

The config file uses the key "prefix" nested under "branch" (see templates/config.json), but this code is looking for "branch_prefix" at the root level. This should be $config.branch.prefix to match the documented JSON structure:

{
  "branch": {
    "prefix": "feature/"
  }
}
Suggested change
if ($config.branch_prefix) {
return $config.branch_prefix
if ($config.branch -and $config.branch.prefix) {
return $config.branch.prefix

Copilot uses AI. Check for mistakes.
Comment on lines +297 to +298
# Extract branch_prefix from config (avoid jq dependency)
local branch_prefix=$(grep '"branch_prefix"' "$config_file" | sed 's/.*"branch_prefix"[^"]*"\([^"]*\)".*/\1/')
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

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

The config file uses the key "prefix" nested under "branch", but this code is looking for "branch_prefix" at the root level. The grep/sed pattern should match the nested structure. For example:

local branch_prefix=$(grep -A 1 '"branch"' "$config_file" | grep '"prefix"' | sed 's/.*"prefix"[^"]*"\([^"]*\)".*/\1/')

Or use a JSON parser if available (e.g., jq -r '.branch.prefix // ""').

Suggested change
# Extract branch_prefix from config (avoid jq dependency)
local branch_prefix=$(grep '"branch_prefix"' "$config_file" | sed 's/.*"branch_prefix"[^"]*"\([^"]*\)".*/\1/')
# Extract branch prefix from config (avoid jq dependency)
local branch_prefix=$(grep -A 10 '"branch"' "$config_file" | grep '"prefix"' | sed 's/.*"prefix"[^"]*"\([^"]*\)".*/\1/')

Copilot uses AI. Check for mistakes.
```

This creates:
- Spec directory: `specs/123-fix-payment-timeout/`
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

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

This documentation is inconsistent with the actual implementation. When using --branch-prefix bugfix/, the spec directory will be specs/bugfix/123-fix-payment-timeout/ (including the prefix), not specs/123-fix-payment-timeout/ as stated here. The git branch correctly includes the prefix as shown.

Suggested change
- Spec directory: `specs/123-fix-payment-timeout/`
- Spec directory: `specs/bugfix/123-fix-payment-timeout/`

Copilot uses AI. Check for mistakes.
Comment on lines +125 to +129
Get-ChildItem -Path $specsDir -Directory | Where-Object {
# Check if directory name contains our number and matches the pattern
$_.Name -match "^(([a-z]+/)?$number)-"
} | ForEach-Object {
$matchedDirs += $_.Name
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

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

When $branchName contains a slash (e.g., "feature/001-user-auth"), Join-Path creates a nested directory structure (specs/feature/001-user-auth/). However, Get-ChildItem -Path $specsDir -Directory only returns immediate children of specs/, so it won't find the nested directories. This will cause Find-FeatureDirByPrefix to fail when looking for prefixed branches.

Consider using -Recurse -Depth 1 or searching in both $specsDir and $specsDir/* subdirectories to handle the nested structure created by prefixed branches.

Suggested change
Get-ChildItem -Path $specsDir -Directory | Where-Object {
# Check if directory name contains our number and matches the pattern
$_.Name -match "^(([a-z]+/)?$number)-"
} | ForEach-Object {
$matchedDirs += $_.Name
# Search recursively for directories up to depth 2 (immediate children and their subdirectories)
Get-ChildItem -Path $specsDir -Directory -Recurse | Where-Object {
# Get the relative path from specs/ to the directory
$relativePath = $_.FullName.Substring($specsDir.Length + 1)
# Check if relative path matches the pattern for numeric prefix
$relativePath -match "^(([a-z]+/)?$number)-"
} | ForEach-Object {
$matchedDirs += $_.FullName.Substring($specsDir.Length + 1)

Copilot uses AI. Check for mistakes.
Comment on lines +113 to +120
# Use find to search more precisely - avoid glob matching issues
while IFS= read -r -d '' dir; do
local dirname=$(basename "$dir")
# Verify it actually matches our pattern: starts with optional prefix/ then number-
if [[ "$dirname" =~ ^(([a-z]+/)?$number)- ]]; then
matches+=("$dirname")
fi
done
done < <(find "$specs_dir" -mindepth 1 -maxdepth 1 -type d -print0)
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

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

When $branch_name contains a slash (e.g., "feature/001-user-auth"), the directory structure created is nested (specs/feature/001-user-auth/). However, find "$specs_dir" -mindepth 1 -maxdepth 1 only searches immediate children of specs/, so it will find specs/feature/ but not specs/feature/001-user-auth/. The basename of specs/feature/ is just feature, which won't match the pattern ^(([a-z]+/)?$number)-.

Consider using -maxdepth 2 and adjusting the logic to handle the nested structure, or use a different approach to find prefixed branch directories.

Copilot uses AI. Check for mistakes.
# Account for: feature number (3) + hyphen (1) = 4 chars
MAX_SUFFIX_LENGTH=$((MAX_BRANCH_LENGTH - 4))
# Account for: prefix length + feature number (3) + hyphen (1)
local prefix_length=${#BRANCH_PREFIX}
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

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

Using local keyword outside of a function is a bash extension and may not work in POSIX-compliant shells. Since this is already at the top level of the script (not inside get_branch_prefix function), this should just be prefix_length=${#BRANCH_PREFIX} without the local keyword.

Suggested change
local prefix_length=${#BRANCH_PREFIX}
prefix_length=${#BRANCH_PREFIX}

Copilot uses AI. Check for mistakes.
CHANGELOG.md Outdated

## [Unreleased]

## [0.0.23] - 2025-10-22 / 2025-11-23
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

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

The version date appears to be in the future: "2025-10-22 / 2025-11-23". Given that my knowledge cutoff is January 2025 and the current date is November 2025, the first date (October 2025) would be in the past, but the dual-date format is unusual. This should likely be a single date in the format YYYY-MM-DD. Please verify the correct release date.

Suggested change
## [0.0.23] - 2025-10-22 / 2025-11-23
## [0.0.23] - 2025-11-23

Copilot uses AI. Check for mistakes.
CHANGELOG.md Outdated
Comment on lines +17 to +24
- New `.specify/config.json` configuration file with `branch.prefix` setting
- Environment variable `SPECIFY_BRANCH_PREFIX` for per-session overrides
- **Per-feature override**: `--branch-prefix` / `-BranchPrefix` parameter for `create-new-feature` scripts
- Priority order: Command-line parameter > Environment variable > Config file > Default (no prefix)
- Automatically created during project initialization via `specify init`
- Examples:
- With `"prefix": "feature/"`: `001-user-auth` → `feature/001-user-auth`
- With `"prefix": "bugfix/"`: `001-fix-login` → `bugfix/001-fix-login`
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

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

The CHANGELOG states the config uses branch.prefix (which matches templates/config.json), but the actual code in both Bash and PowerShell scripts is looking for branch_prefix at the root level instead of the nested branch.prefix structure. This discrepancy between documentation and implementation needs to be resolved - either fix the code to match the documented structure or update the documentation.

Suggested change
- New `.specify/config.json` configuration file with `branch.prefix` setting
- Environment variable `SPECIFY_BRANCH_PREFIX` for per-session overrides
- **Per-feature override**: `--branch-prefix` / `-BranchPrefix` parameter for `create-new-feature` scripts
- Priority order: Command-line parameter > Environment variable > Config file > Default (no prefix)
- Automatically created during project initialization via `specify init`
- Examples:
- With `"prefix": "feature/"`: `001-user-auth` → `feature/001-user-auth`
- With `"prefix": "bugfix/"`: `001-fix-login` → `bugfix/001-fix-login`
- New `.specify/config.json` configuration file with `branch_prefix` setting
- Environment variable `SPECIFY_BRANCH_PREFIX` for per-session overrides
- **Per-feature override**: `--branch-prefix` / `-BranchPrefix` parameter for `create-new-feature` scripts
- Priority order: Command-line parameter > Environment variable > Config file > Default (no prefix)
- Automatically created during project initialization via `specify init`
- Examples:
- With `"branch_prefix": "feature/"`: `001-user-auth` → `feature/001-user-auth`
- With `"branch_prefix": "bugfix/"`: `001-fix-login` → `bugfix/001-fix-login`

Copilot uses AI. Check for mistakes.
@Alexandre-Nourissier
Copy link

Alexandre-Nourissier commented Jan 2, 2026

@ricardojmendez Thank you for this contribution, I am really looking forward to it.

As it stand, it helps with the prefix of branches (feature/, bugfix/, etc...), but there is still the question of branch naming. It is less of a blocker, but some organizations have naming rules such as : <WorkItem-Key>_name_of_the_branch, where WorkItem-Key can be coming from Jira, Azure DevOps, etc...., and have an alphanumerical format including a shorthand for the project, such as "PROJECTKEY-1" (example: "feature/SHOP-39_add_coupons_to_cart"). It is also a way for work planning tools to automatically keep track of implementation work.

So your approach is not addressing this aspect as it is. It seems that this would require a decent amount of configuration, (but can be simplified for the end user, since it is a convention).

That said, I am favorable with doing it step by step, and deliver the value of your PR as quick as possible, so we can consider going further at a later date.

@Shehab-Muhammad
Copy link

Shehab-Muhammad commented Jan 17, 2026

Thanks so much for your effort @ricardojmendez
However, I don't understand why it should be opinionated when it comes to branch name
branch names should be fully configurable
for example we are using clickup for tickets and we need to link our branchs to tickets on clickup
and it has its naming convention (includes ticket number, feature name and auther name)
I think the best way is to suggest the default existing way on specify init should ask user about branch naming convention if it's ok to use defult naming convention 001-user-auth or enter custom naming convention or just ignore branchs and let user handle it manually
I think in the future it should git worktrees this's very important as well

@ricardojmendez
Copy link
Author

fwiw... On my tests, /specify seems to be perfectly fine with you creating the branch yourself and telling it to Use the existing branch.

You may need to update the branch validation code to avoid it grumbling, but I don't believe that it having an issue with the naming is a showstopper.

@Shehab-Muhammad
Copy link

I think it should be supported, I don't have to change branch validation myself on each repo

Copy link

@djeffersonx djeffersonx left a comment

Choose a reason for hiding this comment

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

.

@mnriem mnriem removed the request for review from localden March 9, 2026 13:11
@mnriem
Copy link
Collaborator

mnriem commented Mar 9, 2026

@ricardojmendez As you said "Contains several commits allowing the user to specify some values around branch and spec name" can you break it up in multiple PRs. Specifically bug fixes should not tag along new features and vice versa

@ricardojmendez
Copy link
Author

@mnriem

"Contains several commits allowing the user to specify some values around branch and spec name"
bug fixes should not tag along new features and vice versa

Not sure which part of that indicates that this includes bug fixes, or even features beyond the branch and naming (which go hand in hand).

@mnriem
Copy link
Collaborator

mnriem commented Mar 9, 2026

I was referring to "An arbitrary spec number, to potentially tie it into the corresponding issue on Github, instead of just using a sequence." There were issues filed around that one. So I want to isolate that one from any other commit since it would address a bug (in this case we should validate if that one is no longer needed).

@ricardojmendez
Copy link
Author

I was referring to "An arbitrary spec number, to potentially tie it into the corresponding issue on Github, instead of just using a sequence." There were issues filed around that one.

To be clear: this is not meant to address a bug on spec-kit, but so that a spec-kit user can say "this spec we are working on is for bugfix XYZ", which is internal to their project.

This is a self-contained PR specifically around branch naming. It does what it says on the tin.

If you think it's worth merging, I can figure out the current conflicts. If you don't, or still believe this addresses multiple unrelated issues, please do reject it.

@mnriem
Copy link
Collaborator

mnriem commented Mar 9, 2026

OK, please. address the merge conflicts and the feedback Copilot gave where applicable. If not applicable please describe why it is not. Also make sure this is backwards compatible. Thanks a lot!

Copilot AI review requested due to automatic review settings March 10, 2026 10:05
@ricardojmendez ricardojmendez requested a review from mnriem as a code owner March 10, 2026 10:05
@ricardojmendez
Copy link
Author

@mnriem Fixed the couple conflicting files and pushed again.

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 7 out of 7 changed files in this pull request and generated 13 comments.

Comments suppressed due to low confidence (1)

scripts/bash/common.sh:49

  • This non-git fallback scans only top-level entries in specs/ and expects names to include / (e.g., feature/001-name), which can’t exist as a single directory name. If prefixed branches create nested spec directories, this logic won’t detect the latest feature. Either recurse and return a relative path (feature/001-name) or keep spec directories flat.
    if [[ -d "$specs_dir" ]]; then
        local latest_feature=""
        local highest=0

        for dir in "$specs_dir"/*; do
            if [[ -d "$dir" ]]; then
                local dirname=$(basename "$dir")
                # Support both formats: 001-name or feature/001-name
                if [[ "$dirname" =~ ^(([a-z]+/)?([0-9]{3,}))- ]]; then
                    local number=${BASH_REMATCH[3]}
                    number=$((10#$number))
                    if [[ "$number" -gt "$highest" ]]; then
                        highest=$number
                        latest_feature=$dirname
                    fi
                fi
            fi

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

Comment on lines 324 to 329
if [ ${#BRANCH_NAME} -gt $MAX_BRANCH_LENGTH ]; then
# Calculate how much we need to trim from suffix
# Account for: feature number (3) + hyphen (1) = 4 chars
MAX_SUFFIX_LENGTH=$((MAX_BRANCH_LENGTH - 4))
# Account for: prefix length + feature number (3) + hyphen (1)
local prefix_length=${#BRANCH_PREFIX}
MAX_SUFFIX_LENGTH=$((MAX_BRANCH_LENGTH - prefix_length - 4))

Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

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

local prefix_length=... is used outside of any function. In bash, local is only valid inside a function, so this will error and abort the script when a branch name needs truncation. Remove local here (or wrap this truncation logic in a function).

Copilot uses AI. Check for mistakes.
Comment on lines 94 to 121
find_feature_dir_by_prefix() {
local repo_root="$1"
local branch_name="$2"
local specs_dir="$repo_root/specs"

# Extract numeric prefix from branch (e.g., "004" from "004-whatever")
if [[ ! "$branch_name" =~ ^([0-9]{3})- ]]; then
# Extract numeric prefix from branch (e.g., "004" from "004-whatever" or "feature/004-whatever")
# Pattern: optional prefix (feature/, bugfix/, etc.) followed by at least 3 digits
if [[ ! "$branch_name" =~ ^(([a-z]+/)?([0-9]{3,}))- ]]; then
# If branch doesn't have numeric prefix, fall back to exact match
echo "$specs_dir/$branch_name"
return
fi

local prefix="${BASH_REMATCH[1]}"
local number="${BASH_REMATCH[3]}" # Just the numeric part

# Search for directories in specs/ that start with this prefix
# Search for directories in specs/ that contain this number
# Could be in format: 004-name or feature/004-name or bugfix/004-name
local matches=()
if [[ -d "$specs_dir" ]]; then
for dir in "$specs_dir"/"$prefix"-*; do
if [[ -d "$dir" ]]; then
matches+=("$(basename "$dir")")
# Use find to search more precisely - avoid glob matching issues
while IFS= read -r -d '' dir; do
local dirname=$(basename "$dir")
# Verify it actually matches our pattern: starts with optional prefix/ then number-
if [[ "$dirname" =~ ^(([a-z]+/)?$number)- ]]; then
matches+=("$dirname")
fi
done
done < <(find "$specs_dir" -mindepth 1 -maxdepth 1 -type d -print0)
fi
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

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

find_feature_dir_by_prefix searches only maxdepth=1 and matches directory basenames against patterns containing /. If spec dirs are created using BRANCH_NAME with a prefix (e.g., specs/feature/001-name), this function won’t find them and will fall back to a non-existent path. Either recurse (so you can return specs/feature/001-name) or normalize spec directory names so they don’t include /.

Copilot uses AI. Check for mistakes.
Comment on lines +293 to +303
# Check config file
local config_file="$REPO_ROOT/.specify/config.json"
if [ -f "$config_file" ]; then
# Extract branch_prefix from config (avoid jq dependency)
local branch_prefix=$(grep '"branch_prefix"' "$config_file" | sed 's/.*"branch_prefix"[^"]*"\([^"]*\)".*/\1/')
if [ -n "$branch_prefix" ]; then
echo "$branch_prefix"
return
fi
fi

Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

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

get_branch_prefix() extracts branch_prefix from .specify/config.json, but the documented/template config uses {"branch": {"prefix": ...}}. With the current grep/sed, config-based prefixes will never be applied. Update the parser to read branch.prefix (or change the config schema to branch_prefix) and keep README/examples in sync.

Copilot uses AI. Check for mistakes.
Comment on lines 42 to 48
Get-ChildItem -Path $specsDir -Directory | ForEach-Object {
if ($_.Name -match '^(\d{3})-') {
$num = [int]$matches[1]
# Support both formats: 001-name or feature/001-name
if ($_.Name -match '^(([a-z]+/)?(\d{3,}))-') {
$num = [int]$matches[3]
if ($num -gt $highest) {
$highest = $num
$latestFeature = $_.Name
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

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

The regex here expects directory names under specs/ to contain a literal / (e.g., feature/001-name), but / is a path separator—those will be nested directories (specs/feature/001-name), and Get-ChildItem -Directory at this level will only see feature, not feature/001-name. Either keep spec dirs flat (store BRANCH_NAME without /), or update this logic to recurse and return a relative path like feature/001-name.

Copilot uses AI. Check for mistakes.
Comment on lines +325 to +333
You can configure a default branch prefix for your project that will be applied to all auto-generated branch names:

```json
{
"branch": {
"prefix": "feature/"
}
}
```
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

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

README documents .specify/config.json with a nested branch.prefix, but the scripts currently read a top-level branch_prefix. This mismatch means users following the README example won’t see any effect. Update either the scripts or the documentation so they agree on the config schema.

Copilot uses AI. Check for mistakes.
Comment on lines 221 to +225
- You must only ever run this script once per feature
- The JSON is provided in the terminal as output - always refer to it to get the actual content you're looking for
- The JSON output will contain BRANCH_NAME and SPEC_FILE paths
- For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot")
- You must only ever run this script once per feature
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

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

The “IMPORTANT” list repeats the same instruction twice (“You must only ever run this script once per feature”), which makes the template harder to maintain. Remove the duplicate line to keep this section concise.

Copilot uses AI. Check for mistakes.
Comment on lines +259 to +266
# Check config file
$configFile = Join-Path $RepoRoot '.specify/config.json'
if (Test-Path $configFile) {
try {
$config = Get-Content $configFile -Raw | ConvertFrom-Json
if ($config.branch_prefix) {
return $config.branch_prefix
}
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

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

Get-BranchPrefix reads .specify/config.json looking for $config.branch_prefix, but the template/README describe a nested branch.prefix shape. As a result, config-driven prefixes won't work. Align the config schema (and avoid silently ignoring config parse errors if this is user-facing).

Copilot uses AI. Check for mistakes.
Comment on lines +120 to +131
# Search for directories in specs/ that contain this number
# Could be in format: 004-name or feature/004-name or bugfix/004-name
$matchedDirs = @()

if (Test-Path $specsDir) {
Get-ChildItem -Path $specsDir -Directory | Where-Object {
# Check if directory name contains our number and matches the pattern
$_.Name -match "^(([a-z]+/)?$number)-"
} | ForEach-Object {
$matchedDirs += $_.Name
}
}
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

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

Find-FeatureDirByPrefix only searches immediate children of specs/, and also matches against a pattern containing / in $_ .Name. If branch prefixes are represented as nested directories (specs/feature/001-...), this lookup won’t find the actual spec dir. Either recurse into prefix directories, or change spec dir naming to remain a single directory name (no /).

Copilot uses AI. Check for mistakes.
Comment on lines +28 to +31
**How to recognize spec number in user input:**

- `--number <number>` or `-SpecNumber <number>` format
- Keywords like "issue #42", "ticket 123", "for issue 1234"
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

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

This template instructs using -SpecNumber for PowerShell (and mentions it in recognition rules), but create-new-feature.ps1 defines the parameter as -Number. Using -SpecNumber will be ignored / treated as part of the feature description. Update the template to use -Number consistently (or add an alias parameter in the script).

Copilot uses AI. Check for mistakes.
Comment on lines +310 to +319
# Construct branch name with optional prefix
if [ -n "$BRANCH_PREFIX" ]; then
# Ensure prefix ends with /
if [[ ! "$BRANCH_PREFIX" =~ /$ ]]; then
BRANCH_PREFIX="${BRANCH_PREFIX}/"
fi
BRANCH_NAME="${BRANCH_PREFIX}${FEATURE_NUM}-${BRANCH_SUFFIX}"
else
BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}"
fi
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

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

Once a feature/-style prefix is introduced, the existing number discovery logic in this script (branch scanning and specs-dir scanning) still only matches numbers at the beginning of names (^(\d+)). That means branches like feature/001-... and directories under specs/feature/001-... won't contribute to the max-number calculation, causing repeated reuse of low numbers. Update the detection regexes to support optional <prefix>/ and, if prefixes create nested directories, scan subdirectories accordingly.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator

@mnriem mnriem left a comment

Choose a reason for hiding this comment

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

Please address Copilot feedback where applicable. If not applicable please explain why. Thanks!

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.

6 participants