Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,31 @@ All notable changes to the Specify CLI and templates are documented here.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.0.23] - 2026-01-23

### Added

- **Customizable Branch Naming Templates**: Teams can now configure branch naming patterns via `.specify/settings.toml`
- New `branch.template` setting supports placeholders: `{number}`, `{short_name}`, `{username}`, `{email_prefix}`
- Per-user number scoping: When using `{username}` prefix, each team member gets their own independent number sequence
- Automatic username resolution from Git config with OS username fallback
- Examples:
- `"{number}-{short_name}"` → `001-add-login` (default, solo developer)
- `"{username}/{number}-{short_name}"` → `johndoe/001-add-login` (team)
- `"feature/{username}/{number}-{short_name}"` → `feature/johndoe/001-add-login`
- Full backward compatibility: Projects without settings files work identically to before
- **Settings File Generation**: New `specify init --settings` command generates a documented settings file
- Use `--force` to overwrite existing settings files
- Settings file includes comprehensive documentation and examples
- **Branch Name Validation**: Generated branch names are validated against Git naming rules before creation
- Validates against forbidden characters, path rules, and GitHub's 244-byte limit
- Clear error messages for invalid configurations

### Changed

- `create-new-feature.sh` and `create-new-feature.ps1` now source common utility functions
- Enhanced error messages for template configuration issues (FR-006)

## [0.0.22] - 2025-11-07

- Support for VS Code/Copilot agents, and moving away from prompts to proper agents with hand-offs.
Expand Down
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ The `specify` command supports the following options:
| `--skip-tls` | Flag | Skip SSL/TLS verification (not recommended) |
| `--debug` | Flag | Enable detailed debug output for troubleshooting |
| `--github-token` | Option | GitHub token for API requests (or set GH_TOKEN/GITHUB_TOKEN env variable) |
| `--settings` | Flag | Generate `.specify/settings.toml` for branch template customization. Can be combined with other flags. |

### Examples

Expand Down Expand Up @@ -238,10 +239,49 @@ specify init my-project --ai claude --debug
# Use GitHub token for API requests (helpful for corporate environments)
specify init my-project --ai claude --github-token ghp_your_token_here

# Generate settings file for branch template customization
specify init --settings

# Check system requirements
specify check
```

### Branch Template Configuration

Teams can customize branch naming patterns by creating a `.specify/settings.toml` file:

```bash
# Generate a settings file with documented options
specify init --settings
```

The settings file supports the following template variables:

| Variable | Description | Example Output |
| ---------------- | -------------------------------------------------------------- | ----------------- |
| `{number}` | Auto-incrementing 3-digit feature number | `001`, `002` |
| `{short_name}` | Generated or provided short feature name | `add-login` |
| `{username}` | Git user.name, normalized (lowercase, hyphens) | `jane-smith` |
| `{email_prefix}` | Portion of Git user.email before the @ symbol | `jsmith` |

**Example templates:**

```toml
# Solo developer (default)
template = "{number}-{short_name}"
# Result: 001-add-login

# Team with username prefix
template = "{username}/{number}-{short_name}"
# Result: johndoe/001-add-login

# Team with feature prefix
template = "feature/{username}/{number}-{short_name}"
# Result: feature/johndoe/001-add-login
```

When using `{username}` or a static prefix, each prefix gets its own independent number sequence, avoiding conflicts between team members.

### Available Slash Commands

After running `specify init`, your AI coding agent will have access to these slash commands for structured development:
Expand Down
24 changes: 24 additions & 0 deletions docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,30 @@ If you prefer to get the templates without checking for the right tools:
uvx --from git+https://github.com/github/spec-kit.git specify init <project_name> --ai claude --ignore-agent-tools
```

### Initialize Settings File

Teams can customize branch naming patterns by initializing a settings file:

```bash
# Settings file only (in current directory)
uvx --from git+https://github.com/github/spec-kit.git specify init --settings

# Combined with full project initialization
uvx --from git+https://github.com/github/spec-kit.git specify init <project_name> --settings --ai copilot

# Or with --here flag for existing projects
uvx --from git+https://github.com/github/spec-kit.git specify init --here --settings --force
```

This creates `.specify/settings.toml` where you can configure the branch template:

```toml
[branch]
template = "{username}/{number}-{short_name}" # e.g., jsmith/001-my-feature
```

Available template variables: `{number}`, `{short_name}`, `{username}`, `{email_prefix}`

## Verification

After initialization, you should see the following commands available in your AI agent:
Expand Down
31 changes: 31 additions & 0 deletions docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,37 @@ uvx --from git+https://github.com/github/spec-kit.git specify init <PROJECT_NAME
uvx --from git+https://github.com/github/spec-kit.git specify init <PROJECT_NAME> --script sh # Force POSIX shell
```

#### Customizing Branch Names

Teams can customize how feature branches are named by initializing a settings file:

```bash
# Settings file only (current directory)
uvx --from git+https://github.com/github/spec-kit.git specify init --settings

# Combined with full project init
uvx --from git+https://github.com/github/spec-kit.git specify init <PROJECT_NAME> --settings --ai copilot

# Or with --here flag
uvx --from git+https://github.com/github/spec-kit.git specify init --here --settings --ai claude
```

This creates `.specify/settings.toml` where you can configure the branch template:

```toml
[branch]
template = "{username}/{number}-{short_name}" # e.g., jsmith/001-my-feature
```

Available template variables:

| Variable | Description | Example |
|----------|-------------|---------|
| `{number}` | Auto-incrementing 3-digit number | `001` |
| `{short_name}` | Kebab-case feature name | `my-feature` |
| `{username}` | Git user.name or OS username | `jsmith` |
| `{email_prefix}` | Part before @ in Git email | `john.smith` |

### Step 2: Define Your Constitution

**In your AI Agent's chat interface**, use the `/speckit.constitution` slash command to establish the core rules and principles for your project. You should provide your project's specific principles as arguments.
Expand Down
1 change: 1 addition & 0 deletions docs/upgrade.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ These files are **never touched** by the upgrade—the template packages don't e

-**Your specifications** (`specs/001-my-feature/spec.md`, etc.) - **CONFIRMED SAFE**
-**Your implementation plans** (`specs/001-my-feature/plan.md`, `tasks.md`, etc.) - **CONFIRMED SAFE**
-**Your settings file** (`.specify/settings.toml`) - **CONFIRMED SAFE**
-**Your source code** - **CONFIRMED SAFE**
-**Your git history** - **CONFIRMED SAFE**

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "specify-cli"
version = "0.0.22"
version = "0.0.23"
description = "Specify CLI, part of GitHub Spec Kit. A tool to bootstrap your projects for Spec-Driven Development (SDD)."
requires-python = ">=3.11"
dependencies = [
Expand Down
202 changes: 202 additions & 0 deletions scripts/bash/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,205 @@ EOF
check_file() { [[ -f "$1" ]] && echo " ✓ $2" || echo " ✗ $2"; }
check_dir() { [[ -d "$1" && -n $(ls -A "$1" 2>/dev/null) ]] && echo " ✓ $2" || echo " ✗ $2"; }

# =============================================================================
# TOML Settings Functions (for branch template customization)
# =============================================================================

# Parse a single key from a TOML file
# Usage: get_toml_value "file.toml" "branch.template"
# Supports dotted keys by searching within [section] blocks
get_toml_value() {
local file="$1"
local key="$2"

[[ ! -f "$file" ]] && return 1

# Handle dotted keys like "branch.template"
if [[ "$key" == *.* ]]; then
local section="${key%%.*}"
local subkey="${key#*.}"
# Find the section and extract the key value
awk -v section="$section" -v key="$subkey" '
BEGIN { in_section = 0 }
/^\[/ {
gsub(/[\[\]]/, "")
in_section = ($0 == section)
}
in_section && $0 ~ "^"key"[[:space:]]*=" {
sub(/^[^=]*=[[:space:]]*/, "")
gsub(/^"|"$/, "") # Remove surrounding quotes
print
exit
}
' "$file"
else
# Simple key without section
grep -E "^${key}[[:space:]]*=" "$file" 2>/dev/null | \
sed 's/.*=[[:space:]]*"\([^"]*\)".*/\1/' | head -1
fi
}

# Load branch template from settings file
# Returns: template string or empty if not found
load_branch_template() {
local repo_root="${1:-$(get_repo_root)}"
local settings_file="$repo_root/.specify/settings.toml"

if [[ -f "$settings_file" ]]; then
get_toml_value "$settings_file" "branch.template"
fi
}

# =============================================================================
# Username and Email Resolution Functions
# =============================================================================

# Resolve {username} variable from Git config or OS fallback
# Returns: normalized username (lowercase, hyphens for special chars)
resolve_username() {
local username
username=$(git config user.name 2>/dev/null || echo "")

if [[ -z "$username" ]]; then
# Fallback to OS username
username="${USER:-${USERNAME:-unknown}}"
fi

# Normalize: lowercase, replace non-alphanumeric with hyphens, collapse multiple hyphens
echo "$username" | tr '[:upper:]' '[:lower:]' | \
sed 's/[^a-z0-9]/-/g' | \
sed 's/-\+/-/g' | \
sed 's/^-//' | \
sed 's/-$//'
}

# Resolve {email_prefix} variable from Git config
# Returns: email prefix (portion before @) or empty string
resolve_email_prefix() {
local email
email=$(git config user.email 2>/dev/null || echo "")

if [[ -n "$email" && "$email" == *@* ]]; then
echo "${email%%@*}" | tr '[:upper:]' '[:lower:]'
fi
# Returns empty string if no email configured (per FR-002 clarification)
}

# =============================================================================
# Branch Name Validation Functions
# =============================================================================

# Validate branch name against Git rules
# Args: $1 = branch name
# Returns: 0 if valid, 1 if invalid (prints error to stderr)
validate_branch_name() {
local name="$1"

# Cannot be empty
if [[ -z "$name" ]]; then
echo "Error: Branch name cannot be empty" >&2
return 1
fi

# Cannot start with hyphen
if [[ "$name" == -* ]]; then
echo "Error: Branch name cannot start with hyphen: $name" >&2
Comment on lines +241 to +259
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

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

The branch validation logic earlier in this file (check_feature_branch using the ^[0-9]{3}- pattern) is now inconsistent with the new template-based naming introduced here (e.g., {username}/{number}-{short_name}), which produces branches like jdoe/001-add-login that no longer start with a 3‑digit prefix. Scripts such as setup-plan.sh that call check_feature_branch will reject branches created by create-new-feature.sh when a prefix is configured, breaking the workflow for teams that customize branch.template. The validation should be updated to accept the configured template pattern (or at least to locate and validate the numeric {number} portion after any prefix) instead of hard‑coding a leading NNN- requirement, and related error messages should be generalized away from 001-feature-name.

Copilot uses AI. Check for mistakes.
return 1
fi

# Cannot contain ..
if [[ "$name" == *..* ]]; then
echo "Error: Branch name cannot contain '..': $name" >&2
return 1
fi

# Cannot contain forbidden characters: ~ ^ : ? * [ \
if [[ "$name" =~ [~\^:\?\*\[\\] ]]; then
echo "Error: Branch name contains invalid characters (~^:?*[\\): $name" >&2
return 1
fi

# Cannot end with .lock
if [[ "$name" == *.lock ]]; then
echo "Error: Branch name cannot end with '.lock': $name" >&2
return 1
fi

# Cannot end with /
if [[ "$name" == */ ]]; then
echo "Error: Branch name cannot end with '/': $name" >&2
return 1
fi

# Cannot contain //
if [[ "$name" == *//* ]]; then
echo "Error: Branch name cannot contain '//': $name" >&2
return 1
fi

# Check max length (244 bytes for GitHub)
if [[ ${#name} -gt 244 ]]; then
echo "Warning: Branch name exceeds 244 bytes (GitHub limit): $name" >&2
# Return success but warn - truncation handled elsewhere
fi

return 0
}

# =============================================================================
# Per-User Number Scoping Functions
# =============================================================================

# Get highest feature number for a specific prefix pattern
# Args: $1 = prefix (e.g., "johndoe/" or "feature/johndoe/")
# Returns: highest number found (0 if none)
get_highest_for_prefix() {
local prefix="$1"
local repo_root="${2:-$(get_repo_root)}"
local specs_dir="$repo_root/specs"
local highest=0

# Escape special regex characters in prefix for grep
local escaped_prefix
escaped_prefix=$(printf '%s' "$prefix" | sed 's/[.[\*^$()+?{|\\]/\\&/g')

# Check specs directory for matching directories
if [[ -d "$specs_dir" ]]; then
for dir in "$specs_dir"/"${prefix}"*; do
[[ -d "$dir" ]] || continue
local dirname
dirname=$(basename "$dir")
# Extract number after prefix: prefix + 3-digit number
if [[ "$dirname" =~ ^${escaped_prefix}([0-9]{3})- ]]; then
local num=$((10#${BASH_REMATCH[1]}))
if [[ "$num" -gt "$highest" ]]; then
highest=$num
fi
fi
done
fi

# Also check git branches if available
if git rev-parse --show-toplevel >/dev/null 2>&1; then
local branches
branches=$(git branch -a 2>/dev/null || echo "")
if [[ -n "$branches" ]]; then
while IFS= read -r branch; do
# Clean branch name
local clean_branch
clean_branch=$(echo "$branch" | sed 's/^[* ]*//; s|^remotes/[^/]*/||')

# Check if branch matches prefix pattern
if [[ "$clean_branch" =~ ^${escaped_prefix}([0-9]{3})- ]]; then
local num=$((10#${BASH_REMATCH[1]}))
if [[ "$num" -gt "$highest" ]]; then
highest=$num
fi
fi
done <<< "$branches"
Comment on lines +315 to +352
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

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

get_highest_for_prefix assumes that spec directories' basenames start with the prefix string (regex ^${escaped_prefix}([0-9]{3})-), but FEATURE_DIR is created as $SPECS_DIR/$BRANCH_NAME, so when the prefix contains path separators (e.g. {username}/ or feature/{username}/), the actual spec directory layout is specs/<prefix>/<NNN>-<suffix> and the basename is only NNN-suffix. As a result, this function never detects existing specs for prefixes that include /, so in non‑git repos or when branches are missing it will always return 0 and break per‑prefix numbering. Consider matching against the full relative path (or iterating recursively) instead of just the basename so that prefixes containing / correctly scope their numeric sequences.

Copilot uses AI. Check for mistakes.
fi
fi

echo "$highest"
}

Loading
Loading