Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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
9 changes: 9 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ The toolkit supports multiple AI coding assistants, allowing teams to use their
## General practices

- Any changes to `__init__.py` for the Specify CLI require a version rev in `pyproject.toml` and addition of entries to `CHANGELOG.md`.
- Environment variables that affect script behavior should be documented in both `README.md` and `CHANGELOG.md`.

### Environment Variables

The Spec Kit workflow recognizes the following environment variables:

- **`SPECIFY_FEATURE`**: Override feature detection. Set to a specific feature directory name (e.g., `001-photo-albums`) to work on that feature regardless of git branch or directory scan results. This has the highest priority in feature detection.

- **`SPECIFY_USE_CURRENT_BRANCH`**: Use the current git branch name as the feature identifier without creating a new branch. Useful when working on existing branches that don't follow the `###-name` convention. Works with any branch name. Priority: below `SPECIFY_FEATURE`, above default git detection.

## Adding New Agent Support

Expand Down
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,21 @@ 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).

## [Unreleased]

### Added

- **Git Branch Integration with `SPECIFY_USE_CURRENT_BRANCH`**: New environment variable to use the current git branch name as the feature identifier without creating a new branch
- Enables working on existing branches that don't follow the `###-name` convention
- Handles edge cases: detached HEAD state, non-git repositories, git command failures
- Maintains existing priority chain: `SPECIFY_FEATURE` > `SPECIFY_USE_CURRENT_BRANCH` > git branch > directory scan > "main"
- Single git command execution (no redundancy)
- Examples:
- `export SPECIFY_USE_CURRENT_BRANCH=1` (bash)
- `$env:SPECIFY_USE_CURRENT_BRANCH="1"` (PowerShell)
- Works with any branch naming pattern (feature/, bugfix/, hotfix/, main, master, develop, etc.)
- Available in both bash and PowerShell scripts

## [0.0.20] - 2025-10-14

### Added
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ Additional commands for enhanced quality and validation:
| Variable | Description |
|------------------|------------------------------------------------------------------------------------------------|
| `SPECIFY_FEATURE` | Override feature detection for non-Git repositories. Set to the feature directory name (e.g., `001-photo-albums`) to work on a specific feature when not using Git branches.<br/>**Must be set in the context of the agent you're working with prior to using `/speckit.plan` or follow-up commands. |
| `SPECIFY_USE_CURRENT_BRANCH` | Use the current git branch name as the feature identifier without creating a new branch. Useful when working on existing branches that don't follow the `###-name` convention.<br/>**Example:** `export SPECIFY_USE_CURRENT_BRANCH=1` (bash) or `$env:SPECIFY_USE_CURRENT_BRANCH="1"` (PowerShell)<br/>**Note:** `SPECIFY_FEATURE` takes precedence if both are set. |

## 📚 Core Philosophy

Expand Down
50 changes: 50 additions & 0 deletions scripts/bash/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,29 @@ get_repo_root() {
fi
}

# Sanitize branch name for use as directory name
# Replaces filesystem-forbidden and problematic characters with safe alternatives
sanitize_branch_name() {
local branch="$1"

# Replace problematic characters:
# / → - (prevents nesting on all platforms, Windows forbidden)
# \ → - (Windows forbidden)
# : → - (Windows forbidden, macOS translated)
# * → - (Windows forbidden, shell wildcard)
# ? → - (Windows forbidden, shell wildcard)
# " → - (Windows forbidden)
# < → - (Windows forbidden, shell redirect)
# > → - (Windows forbidden, shell redirect)
# | → - (Windows forbidden, shell pipe)
echo "$branch" | sed \
-e 's/[\/\\:*?"<>|]/-/g' \
-e 's/ */-/g' \
-e 's/^[. -]*//' \
-e 's/[. -]*$//' \
-e 's/--*/-/g'
}

# Get current branch, with fallback for non-git repositories
get_current_branch() {
# First check if SPECIFY_FEATURE environment variable is set
Expand All @@ -20,6 +43,20 @@ get_current_branch() {
return
fi

# Check if SPECIFY_USE_CURRENT_BRANCH is set to use current git branch
# without creating a new feature branch
if [[ -n "${SPECIFY_USE_CURRENT_BRANCH:-}" ]]; then
local current_git_branch
if current_git_branch=$(git rev-parse --abbrev-ref HEAD 2>&1); then
if [[ "$current_git_branch" != "HEAD" ]]; then
# Valid branch (not detached HEAD) - use it
echo "$current_git_branch"
return
fi
fi
# Detached HEAD or git command failed - fall through to normal behavior
fi

# Then check git if available
if git rev-parse --abbrev-ref HEAD >/dev/null 2>&1; then
git rev-parse --abbrev-ref HEAD
Expand Down Expand Up @@ -72,6 +109,11 @@ check_feature_branch() {
return 0
fi

# If SPECIFY_USE_CURRENT_BRANCH is set, skip pattern validation
if [[ -n "${SPECIFY_USE_CURRENT_BRANCH:-}" ]]; then
return 0
fi

if [[ ! "$branch" =~ ^[0-9]{3}- ]]; then
echo "ERROR: Not on a feature branch. Current branch: $branch" >&2
echo "Feature branches should be named like: 001-feature-name" >&2
Expand All @@ -90,6 +132,14 @@ find_feature_dir_by_prefix() {
local branch_name="$2"
local specs_dir="$repo_root/specs"

# When using current branch, do exact match only (no prefix matching)
if [[ -n "${SPECIFY_USE_CURRENT_BRANCH:-}" ]]; then
# Sanitize branch name for filesystem compatibility
local sanitized_name=$(sanitize_branch_name "$branch_name")
echo "$specs_dir/$sanitized_name"
return
fi

# Extract numeric prefix from branch (e.g., "004" from "004-whatever")
if [[ ! "$branch_name" =~ ^([0-9]{3})- ]]; then
# If branch doesn't have numeric prefix, fall back to exact match
Expand Down
101 changes: 75 additions & 26 deletions scripts/bash/create-new-feature.sh
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,29 @@ if [ -z "$FEATURE_DESCRIPTION" ]; then
exit 1
fi

# Sanitize branch name for use as directory name
# Replaces filesystem-forbidden and problematic characters with safe alternatives
sanitize_branch_name() {
local branch="$1"

# Replace problematic characters:
# / → - (prevents nesting on all platforms, Windows forbidden)
# \ → - (Windows forbidden)
# : → - (Windows forbidden, macOS translated)
# * → - (Windows forbidden, shell wildcard)
# ? → - (Windows forbidden, shell wildcard)
# " → - (Windows forbidden)
# < → - (Windows forbidden, shell redirect)
# > → - (Windows forbidden, shell redirect)
# | → - (Windows forbidden, shell pipe)
echo "$branch" | sed \
-e 's/[\/\\:*?"<>|]/-/g' \
-e 's/ */-/g' \
-e 's/^[. -]*//' \
-e 's/[. -]*$//' \
-e 's/--*/-/g'
}

# Function to find the repository root by searching for existing project markers
find_repo_root() {
local dir="$1"
Expand Down Expand Up @@ -210,34 +233,60 @@ if [ -z "$BRANCH_NUMBER" ]; then
fi
fi

FEATURE_NUM=$(printf "%03d" "$BRANCH_NUMBER")
BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}"
# Check if SPECIFY_USE_CURRENT_BRANCH is set
if [[ -n "${SPECIFY_USE_CURRENT_BRANCH:-}" ]]; then
if [ "$HAS_GIT" = true ]; then
# Use current branch name
branch_name_output=$(git rev-parse --abbrev-ref HEAD 2>&1)
branch_name_status=$?
if [[ $branch_name_status -ne 0 || "$branch_name_output" == "HEAD" ]]; then
>&2 echo "[specify] Error: Cannot determine current branch name"
exit 1
fi
# Sanitize branch name for filesystem compatibility
original_branch="$branch_name_output"
BRANCH_NAME=$(sanitize_branch_name "$branch_name_output")
FEATURE_NUM="N/A"
if [[ "$original_branch" != "$BRANCH_NAME" ]]; then
>&2 echo "[specify] Using current branch: $original_branch (sanitized to: $BRANCH_NAME)"
else
>&2 echo "[specify] Using current branch: $BRANCH_NAME"
fi
else
>&2 echo "[specify] Error: SPECIFY_USE_CURRENT_BRANCH requires a git repository"
exit 1
fi
else
# Normal mode: generate new branch name and create it
FEATURE_NUM=$(printf "%03d" "$BRANCH_NUMBER")
BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}"

# GitHub enforces a 244-byte limit on branch names
# Validate and truncate if necessary
MAX_BRANCH_LENGTH=244
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))

# Truncate suffix at word boundary if possible
TRUNCATED_SUFFIX=$(echo "$BRANCH_SUFFIX" | cut -c1-$MAX_SUFFIX_LENGTH)
# Remove trailing hyphen if truncation created one
TRUNCATED_SUFFIX=$(echo "$TRUNCATED_SUFFIX" | sed 's/-$//')

ORIGINAL_BRANCH_NAME="$BRANCH_NAME"
BRANCH_NAME="${FEATURE_NUM}-${TRUNCATED_SUFFIX}"

>&2 echo "[specify] Warning: Branch name exceeded GitHub's 244-byte limit"
>&2 echo "[specify] Original: $ORIGINAL_BRANCH_NAME (${#ORIGINAL_BRANCH_NAME} bytes)"
>&2 echo "[specify] Truncated to: $BRANCH_NAME (${#BRANCH_NAME} bytes)"
fi
# GitHub enforces a 244-byte limit on branch names
# Validate and truncate if necessary
MAX_BRANCH_LENGTH=244
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))

if [ "$HAS_GIT" = true ]; then
git checkout -b "$BRANCH_NAME"
else
>&2 echo "[specify] Warning: Git repository not detected; skipped branch creation for $BRANCH_NAME"
# Truncate suffix at word boundary if possible
TRUNCATED_SUFFIX=$(echo "$BRANCH_SUFFIX" | cut -c1-$MAX_SUFFIX_LENGTH)
# Remove trailing hyphen if truncation created one
TRUNCATED_SUFFIX=$(echo "$TRUNCATED_SUFFIX" | sed 's/-$//')

ORIGINAL_BRANCH_NAME="$BRANCH_NAME"
BRANCH_NAME="${FEATURE_NUM}-${TRUNCATED_SUFFIX}"

>&2 echo "[specify] Warning: Branch name exceeded GitHub's 244-byte limit"
>&2 echo "[specify] Original: $ORIGINAL_BRANCH_NAME (${#ORIGINAL_BRANCH_NAME} bytes)"
>&2 echo "[specify] Truncated to: $BRANCH_NAME (${#BRANCH_NAME} bytes)"
fi

if [ "$HAS_GIT" = true ]; then
git checkout -b "$BRANCH_NAME"
else
>&2 echo "[specify] Warning: Git repository not detected; skipped branch creation for $BRANCH_NAME"
fi
fi

FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME"
Expand Down
53 changes: 49 additions & 4 deletions scripts/powershell/common.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,51 @@ function Get-RepoRoot {
} catch {
# Git command failed
}

# Fall back to script location for non-git repos
return (Resolve-Path (Join-Path $PSScriptRoot "../../..")).Path
}

# Sanitize branch name for use as directory name
# Replaces filesystem-forbidden and problematic characters with safe alternatives
function Sanitize-BranchName {
param([string]$BranchName)

# Replace problematic characters:
# / → - (prevents nesting on all platforms, Windows forbidden)
# \ → - (Windows forbidden)
# : → - (Windows forbidden, macOS translated)
# * → - (Windows forbidden, shell wildcard)
# ? → - (Windows forbidden, shell wildcard)
# " → - (Windows forbidden)
# < → - (Windows forbidden, shell redirect)
# > → - (Windows forbidden, shell redirect)
# | → - (Windows forbidden, shell pipe)
$sanitized = $BranchName -replace '[/\\:*?"<>|]', '-'
$sanitized = $sanitized -replace '\s+', '-'
$sanitized = $sanitized -replace '^[. -]+', ''
$sanitized = $sanitized -replace '[. -]+$', ''
$sanitized = $sanitized -replace '-+', '-'
return $sanitized
}

function Get-CurrentBranch {
# First check if SPECIFY_FEATURE environment variable is set
if ($env:SPECIFY_FEATURE) {
return $env:SPECIFY_FEATURE
}


# Check if SPECIFY_USE_CURRENT_BRANCH is set to use current git branch
# without creating a new feature branch
if ($env:SPECIFY_USE_CURRENT_BRANCH) {
$currentGitBranch = git rev-parse --abbrev-ref HEAD 2>&1
if ($LASTEXITCODE -eq 0 -and $currentGitBranch -ne 'HEAD') {
# Valid branch (not detached HEAD) - use it
return $currentGitBranch
}
# Detached HEAD or git command failed - fall through to normal behavior
}

# Then check git if available
try {
$result = git rev-parse --abbrev-ref HEAD 2>$null
Expand Down Expand Up @@ -72,13 +106,18 @@ function Test-FeatureBranch {
[string]$Branch,
[bool]$HasGit = $true
)

# For non-git repos, we can't enforce branch naming but still provide output
if (-not $HasGit) {
Write-Warning "[specify] Warning: Git repository not detected; skipped branch validation"
return $true
}


# If SPECIFY_USE_CURRENT_BRANCH is set, skip pattern validation
if ($env:SPECIFY_USE_CURRENT_BRANCH) {
return $true
}

if ($Branch -notmatch '^[0-9]{3}-') {
Write-Output "ERROR: Not on a feature branch. Current branch: $Branch"
Write-Output "Feature branches should be named like: 001-feature-name"
Expand All @@ -89,6 +128,12 @@ function Test-FeatureBranch {

function Get-FeatureDir {
param([string]$RepoRoot, [string]$Branch)

# When using current branch, sanitize for filesystem compatibility
if ($env:SPECIFY_USE_CURRENT_BRANCH) {
$Branch = Sanitize-BranchName -BranchName $Branch
}

Join-Path $RepoRoot "specs/$Branch"
}

Expand Down
Loading