diff --git a/CHANGELOG.md b/CHANGELOG.md index ea6729145..371dcb8ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,27 @@ 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] + +## [0.0.21] - 2025-10-22 + +### Added + +- **Configurable Branch Prefixes**: Branch names can now be prefixed with custom patterns (e.g., `feature/`, `bugfix/`) + - 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` + - With `SPECIFY_BRANCH_PREFIX=dev/`: Overrides config file setting + - With `--branch-prefix "hotfix/"`: Overrides all other settings for that feature + - Supported in both bash and PowerShell variants of `create-new-feature` scripts + - AI agents can recognize and apply prefix from `/speckit.specify` command (e.g., `/speckit.specify Add login --branch-prefix feature/`) + - Python CLI includes `setup_config_file()` function to initialize configuration during project setup + ## [0.0.20] - 2025-10-14 ### Added diff --git a/README.md b/README.md index 1c7dda215..4ccc44648 100644 --- a/README.md +++ b/README.md @@ -252,6 +252,70 @@ 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.
**Must be set in the context of the agent you're working with prior to using `/speckit.plan` or follow-up commands. | +| `SPECIFY_BRANCH_PREFIX` | Configure a prefix for git branch names (e.g., `feature/`, `bugfix/`). When set, this prefix is prepended to auto-generated branch names. Overrides the `branch.prefix` setting in `.specify/config.json`. Example: With prefix `feature/`, branch `001-user-auth` becomes `feature/001-user-auth`. | +| `SPECIFY_SPEC_NUMBER` | Override the auto-incremented spec number with a custom value (e.g., to match an issue tracker number). When set, the specified number is used instead of finding the next available number. Example: `SPECIFY_SPEC_NUMBER=42` creates spec `042-feature-name`. Can be overridden per-feature using the `--number` parameter in `/speckit.specify`. | + +### Configuration File + +The `.specify/config.json` file allows you to configure project-specific settings. This file is automatically created when you initialize a new project with `specify init`. + +#### Branch Prefix Configuration + +You can configure a default branch prefix for your project that will be applied to all auto-generated branch names: + +```json +{ + "branch": { + "prefix": "feature/" + } +} +``` + +**Common patterns:** + +- **Feature branches:** `"prefix": "feature/"` → Creates branches like `feature/001-user-auth` +- **Bugfix branches:** `"prefix": "bugfix/"` → Creates branches like `bugfix/001-fix-login` +- **Development branches:** `"prefix": "dev/"` → Creates branches like `dev/001-new-api` +- **No prefix (default):** `"prefix": ""` → Creates branches like `001-user-auth` + +**Priority order:** + +1. `--branch-prefix` command-line parameter (highest priority, per-feature override) +2. `SPECIFY_BRANCH_PREFIX` environment variable (per-session override) +3. `.specify/config.json` file setting (project-wide default) +4. Default: no prefix (empty string) + +This allows you to set project-wide defaults in the config file, override them per-session using the environment variable, or specify them per-feature when creating a new specification. + +**Per-feature branch prefix:** + +When using the `/speckit.specify` command, you can specify a branch prefix for that specific feature: + +```text +/speckit.specify Add user authentication --branch-prefix feature/ +/speckit.specify Fix login timeout --branch-prefix bugfix/ +/speckit.specify Update API endpoints with prefix hotfix/ +``` + +The AI agent will recognize the prefix specification and pass it to the `create-new-feature` script. + +**Per-feature spec number:** + +You can also specify a custom spec number to match your issue tracker: + +```text +/speckit.specify Add user authentication --number 42 +/speckit.specify Fix payment bug for issue #123 --number 123 +/speckit.specify Implement search API --number 1234 --branch-prefix feature/ +``` + +**Spec number priority:** + +1. `--number` command-line parameter (highest priority, per-feature override) +2. `SPECIFY_SPEC_NUMBER` environment variable (per-session override) +3. Auto-increment from existing specs (default) + +This allows you to align spec numbers with your issue tracker (GitHub Issues, Jira, Linear, etc.) while maintaining the structured workflow. ## 📚 Core Philosophy @@ -389,6 +453,8 @@ With your project principles established, you can now create the functional spec >[!IMPORTANT] >Be as explicit as possible about *what* you are trying to build and *why*. **Do not focus on the tech stack at this point**. +#### Basic Example + An example prompt: ```text @@ -410,6 +476,32 @@ see yours. You can edit any comments that you make, but you can't edit comments delete any comments that you made, but you can't delete comments anybody else made. ``` +#### Issue Tracker Integration + +You can align spec numbers with your issue tracker (GitHub Issues, Jira, Linear, etc.) by specifying a custom spec number: + +```text +/speckit.specify Add user authentication system --number 42 +``` + +This creates spec `042-add-user-auth` matching issue #42 in your tracker. You can also combine with branch prefixes: + +```text +/speckit.specify Fix payment processing timeout --number 123 --branch-prefix bugfix/ +``` + +This creates: +- Spec directory: `specs/123-fix-payment-timeout/` +- Git branch: `bugfix/123-fix-payment-timeout` + +**Common workflows:** + +- **GitHub Issues:** `--number ` (e.g., `--number 456`) +- **Jira Tickets:** `--number ` (e.g., `--number 789` for PROJ-789) +- **Linear Issues:** `--number ` (e.g., `--number 1234`) + +#### After Running /speckit.specify + After this prompt is entered, you should see Claude Code kick off the planning and spec drafting process. Claude Code will also trigger some of the built-in scripts to set up the repository. Once this step is completed, you should have a new branch created (e.g., `001-create-taskify`), as well as a new specification in the `specs/001-create-taskify` directory. diff --git a/pyproject.toml b/pyproject.toml index 567d48cd4..e4d2791bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "specify-cli" -version = "0.0.20" +version = "0.0.21" description = "Specify CLI, part of GitHub Spec Kit. A tool to bootstrap your projects for Spec-Driven Development (SDD)." requires-python = ">=3.11" dependencies = [ diff --git a/scripts/bash/common.sh b/scripts/bash/common.sh index 6931eccc8..2839e7189 100644 --- a/scripts/bash/common.sh +++ b/scripts/bash/common.sh @@ -37,8 +37,9 @@ get_current_branch() { for dir in "$specs_dir"/*; do if [[ -d "$dir" ]]; then local dirname=$(basename "$dir") - if [[ "$dirname" =~ ^([0-9]{3})- ]]; then - local number=${BASH_REMATCH[1]} + # 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 @@ -72,9 +73,13 @@ check_feature_branch() { return 0 fi - if [[ ! "$branch" =~ ^[0-9]{3}- ]]; then + # Support both simple format (001-name) and prefixed format (feature/001-name) + if [[ ! "$branch" =~ ^([a-z]+/)?[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 + echo "Feature branches should be named like:" >&2 + echo " - 001-feature-name" >&2 + echo " - feature/001-feature-name" >&2 + echo " - bugfix/042-fix-name" >&2 return 1 fi @@ -85,26 +90,33 @@ get_feature_dir() { echo "$1/specs/$2"; } # Find feature directory by numeric prefix instead of exact branch match # This allows multiple branches to work on the same spec (e.g., 004-fix-bug, 004-add-feature) +# Also handles branch names with prefixes like feature/004-name or bugfix/042-fix 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 + for dir in "$specs_dir"/*"$number"-*; do if [[ -d "$dir" ]]; then - matches+=("$(basename "$dir")") + local dirname=$(basename "$dir") + # Verify it actually matches our pattern (not just contains the number) + if [[ "$dirname" =~ ^(([a-z]+/)?$number)- ]]; then + matches+=("$dirname") + fi fi done fi @@ -118,7 +130,7 @@ find_feature_dir_by_prefix() { echo "$specs_dir/${matches[0]}" else # Multiple matches - this shouldn't happen with proper naming convention - echo "ERROR: Multiple spec directories found with prefix '$prefix': ${matches[*]}" >&2 + echo "ERROR: Multiple spec directories found with number '$number': ${matches[*]}" >&2 echo "Please ensure only one spec directory exists per numeric prefix." >&2 echo "$specs_dir/$branch_name" # Return something to avoid breaking the script fi diff --git a/scripts/bash/create-new-feature.sh b/scripts/bash/create-new-feature.sh index 86d9ecf83..43839f931 100644 --- a/scripts/bash/create-new-feature.sh +++ b/scripts/bash/create-new-feature.sh @@ -4,6 +4,7 @@ set -e JSON_MODE=false SHORT_NAME="" +BRANCH_PREFIX_ARG="" BRANCH_NUMBER="" ARGS=() i=1 @@ -27,6 +28,20 @@ while [ $i -le $# ]; do fi SHORT_NAME="$next_arg" ;; + --branch-prefix) + if [ $((i + 1)) -gt $# ]; then + echo 'Error: --branch-prefix requires a value' >&2 + exit 1 + fi + i=$((i + 1)) + next_arg="${!i}" + # Check if the next argument is another option (starts with --) + if [[ "$next_arg" == --* ]]; then + echo 'Error: --branch-prefix requires a value' >&2 + exit 1 + fi + BRANCH_PREFIX_ARG="$next_arg" + ;; --number) if [ $((i + 1)) -gt $# ]; then echo 'Error: --number requires a value' >&2 @@ -41,17 +56,20 @@ while [ $i -le $# ]; do BRANCH_NUMBER="$next_arg" ;; --help|-h) - echo "Usage: $0 [--json] [--short-name ] [--number N] " + echo "Usage: $0 [--json] [--short-name ] [--branch-prefix ] [--number N] " echo "" echo "Options:" - echo " --json Output in JSON format" - echo " --short-name Provide a custom short name (2-4 words) for the branch" - echo " --number N Specify branch number manually (overrides auto-detection)" - echo " --help, -h Show this help message" + echo " --json Output in JSON format" + echo " --short-name Provide a custom short name (2-4 words) for the branch" + echo " --branch-prefix Override branch prefix (e.g., 'feature/', 'bugfix/')" + echo " --number N Specify branch number manually (overrides auto-detection)" + echo " --help, -h Show this help message" echo "" echo "Examples:" echo " $0 'Add user authentication system' --short-name 'user-auth'" echo " $0 'Implement OAuth2 integration for API' --number 5" + echo " $0 'Fix login bug' --branch-prefix 'bugfix/'" + echo " $0 'Add payment processing' --number 42 --branch-prefix 'feature/'" exit 0 ;; *) @@ -211,15 +229,56 @@ if [ -z "$BRANCH_NUMBER" ]; then fi FEATURE_NUM=$(printf "%03d" "$BRANCH_NUMBER") -BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}" + +# Function to determine branch prefix +get_branch_prefix() { + # Priority: CLI arg > environment variable > config file > empty string + if [ -n "$BRANCH_PREFIX_ARG" ]; then + echo "$BRANCH_PREFIX_ARG" + return + fi + + if [ -n "$SPECIFY_BRANCH_PREFIX" ]; then + echo "$SPECIFY_BRANCH_PREFIX" + return + fi + + # 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 + + echo "" +} + +# Determine branch prefix +BRANCH_PREFIX=$(get_branch_prefix) + +# 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 # 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)) + # Account for: prefix length + feature number (3) + hyphen (1) + local prefix_length=${#BRANCH_PREFIX} + MAX_SUFFIX_LENGTH=$((MAX_BRANCH_LENGTH - prefix_length - 4)) # Truncate suffix at word boundary if possible TRUNCATED_SUFFIX=$(echo "$BRANCH_SUFFIX" | cut -c1-$MAX_SUFFIX_LENGTH) @@ -227,7 +286,11 @@ if [ ${#BRANCH_NAME} -gt $MAX_BRANCH_LENGTH ]; then TRUNCATED_SUFFIX=$(echo "$TRUNCATED_SUFFIX" | sed 's/-$//') ORIGINAL_BRANCH_NAME="$BRANCH_NAME" - BRANCH_NAME="${FEATURE_NUM}-${TRUNCATED_SUFFIX}" + if [ -n "$BRANCH_PREFIX" ]; then + BRANCH_NAME="${BRANCH_PREFIX}${FEATURE_NUM}-${TRUNCATED_SUFFIX}" + else + BRANCH_NAME="${FEATURE_NUM}-${TRUNCATED_SUFFIX}" + fi >&2 echo "[specify] Warning: Branch name exceeded GitHub's 244-byte limit" >&2 echo "[specify] Original: $ORIGINAL_BRANCH_NAME (${#ORIGINAL_BRANCH_NAME} bytes)" diff --git a/scripts/powershell/common.ps1 b/scripts/powershell/common.ps1 index b0be27354..f9631ccf0 100644 --- a/scripts/powershell/common.ps1 +++ b/scripts/powershell/common.ps1 @@ -40,8 +40,9 @@ function Get-CurrentBranch { $highest = 0 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 @@ -79,9 +80,13 @@ function Test-FeatureBranch { return $true } - if ($Branch -notmatch '^[0-9]{3}-') { + # Support both simple format (001-name) and prefixed format (feature/001-name) + if ($Branch -notmatch '^([a-z]+/)?[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" + Write-Output "Feature branches should be named like:" + Write-Output " - 001-feature-name" + Write-Output " - feature/001-feature-name" + Write-Output " - bugfix/042-fix-name" return $false } return $true @@ -92,11 +97,61 @@ function Get-FeatureDir { Join-Path $RepoRoot "specs/$Branch" } +# Find feature directory by numeric prefix instead of exact branch match +# This allows multiple branches to work on the same spec (e.g., 004-fix-bug, 004-add-feature) +# Also handles branch names with prefixes like feature/004-name or bugfix/042-fix +function Find-FeatureDirByPrefix { + param( + [string]$RepoRoot, + [string]$BranchName + ) + + $specsDir = Join-Path $RepoRoot "specs" + + # 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 ($BranchName -notmatch '^(([a-z]+/)?(\d{3,}))-') { + # If branch doesn't have numeric prefix, fall back to exact match + return (Join-Path $specsDir $BranchName) + } + + $number = $matches[3] # Just the numeric part + + # 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 + } + } + + # Handle results + if ($matchedDirs.Count -eq 0) { + # No match found - return the branch name path (will fail later with clear error) + return (Join-Path $specsDir $BranchName) + } elseif ($matchedDirs.Count -eq 1) { + # Exactly one match - perfect! + return (Join-Path $specsDir $matchedDirs[0]) + } else { + # Multiple matches - this shouldn't happen with proper naming convention + Write-Warning "ERROR: Multiple spec directories found with number '$number': $($matchedDirs -join ', ')" + Write-Warning "Please ensure only one spec directory exists per numeric prefix." + return (Join-Path $specsDir $BranchName) # Return something to avoid breaking the script + } +} + function Get-FeaturePathsEnv { $repoRoot = Get-RepoRoot $currentBranch = Get-CurrentBranch $hasGit = Test-HasGit - $featureDir = Get-FeatureDir -RepoRoot $repoRoot -Branch $currentBranch + + # Use prefix-based lookup to support multiple branches per spec and branch prefixes + $featureDir = Find-FeatureDirByPrefix -RepoRoot $repoRoot -BranchName $currentBranch [PSCustomObject]@{ REPO_ROOT = $repoRoot diff --git a/scripts/powershell/create-new-feature.ps1 b/scripts/powershell/create-new-feature.ps1 index 4daa6d2c0..64aa32111 100644 --- a/scripts/powershell/create-new-feature.ps1 +++ b/scripts/powershell/create-new-feature.ps1 @@ -5,6 +5,7 @@ param( [switch]$Json, [string]$ShortName, [int]$Number = 0, + [string]$BranchPrefix, [switch]$Help, [Parameter(ValueFromRemainingArguments = $true)] [string[]]$FeatureDescription @@ -13,17 +14,23 @@ $ErrorActionPreference = 'Stop' # Show help if requested if ($Help) { - Write-Host "Usage: ./create-new-feature.ps1 [-Json] [-ShortName ] [-Number N] " + Write-Host "Usage: ./create-new-feature.ps1 [-Json] [-ShortName ] [-Number N] [-BranchPrefix ] " Write-Host "" Write-Host "Options:" - Write-Host " -Json Output in JSON format" - Write-Host " -ShortName Provide a custom short name (2-4 words) for the branch" - Write-Host " -Number N Specify branch number manually (overrides auto-detection)" - Write-Host " -Help Show this help message" + Write-Host " -Json Output in JSON format" + Write-Host " -ShortName Provide a custom short name (2-4 words) for the branch" + Write-Host " -Number N Specify branch number manually (overrides auto-detection)" + Write-Host " -BranchPrefix Specify branch prefix (e.g., 'feature', 'bugfix')" + Write-Host " -Help Show this help message" Write-Host "" Write-Host "Examples:" Write-Host " ./create-new-feature.ps1 'Add user authentication system' -ShortName 'user-auth'" Write-Host " ./create-new-feature.ps1 'Implement OAuth2 integration for API'" + Write-Host " ./create-new-feature.ps1 'Fix login bug' -Number 42 -BranchPrefix 'bugfix'" + Write-Host "" + Write-Host "Environment Variables:" + Write-Host " SPECIFY_SPEC_NUMBER Set default spec/branch number" + Write-Host " SPECIFY_BRANCH_PREFIX Set default branch prefix" exit 0 } @@ -225,15 +232,61 @@ if ($Number -eq 0) { } $featureNum = ('{0:000}' -f $Number) -$branchName = "$featureNum-$branchSuffix" + +# Function to determine branch prefix +function Get-BranchPrefix { + param( + [string]$BranchPrefixArg, + [string]$RepoRoot + ) + + # Priority: CLI arg > environment variable > config file > empty string + if ($BranchPrefixArg) { + return $BranchPrefixArg + } + + if ($env:SPECIFY_BRANCH_PREFIX) { + return $env:SPECIFY_BRANCH_PREFIX + } + + # 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 + } + } catch { + # Ignore config parsing errors + } + } + + return "" +} + +# Determine branch prefix +$branchPrefix = Get-BranchPrefix -BranchPrefixArg $BranchPrefix -RepoRoot $repoRoot + +# Construct branch name with optional prefix +if ($branchPrefix) { + # Ensure prefix ends with / + if (-not $branchPrefix.EndsWith('/')) { + $branchPrefix = "$branchPrefix/" + } + $branchName = "$branchPrefix$featureNum-$branchSuffix" +} else { + $branchName = "$featureNum-$branchSuffix" +} # GitHub enforces a 244-byte limit on branch names # Validate and truncate if necessary $maxBranchLength = 244 if ($branchName.Length -gt $maxBranchLength) { # Calculate how much we need to trim from suffix - # Account for: feature number (3) + hyphen (1) = 4 chars - $maxSuffixLength = $maxBranchLength - 4 + # Account for: prefix length + feature number (3) + hyphen (1) + $prefixLength = $branchPrefix.Length + $maxSuffixLength = $maxBranchLength - $prefixLength - 4 # Truncate suffix $truncatedSuffix = $branchSuffix.Substring(0, [Math]::Min($branchSuffix.Length, $maxSuffixLength)) @@ -241,7 +294,11 @@ if ($branchName.Length -gt $maxBranchLength) { $truncatedSuffix = $truncatedSuffix -replace '-$', '' $originalBranchName = $branchName - $branchName = "$featureNum-$truncatedSuffix" + if ($branchPrefix) { + $branchName = "$branchPrefix$featureNum-$truncatedSuffix" + } else { + $branchName = "$featureNum-$truncatedSuffix" + } Write-Warning "[specify] Branch name exceeded GitHub's 244-byte limit" Write-Warning "[specify] Original: $originalBranchName ($($originalBranchName.Length) bytes)" diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index a33a1c61a..547433178 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -818,6 +818,23 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, script_ return project_path +def setup_config_file(project_path: Path, tracker: StepTracker | None = None) -> None: + """Copy config.json from templates to .specify root if it doesn't already exist.""" + config_template = project_path / ".specify" / "templates" / "config.json" + config_dest = project_path / ".specify" / "config.json" + + # Only copy if template exists and destination doesn't exist (preserve user config) + if config_template.exists() and not config_dest.exists(): + try: + shutil.copy2(config_template, config_dest) + if tracker: + tracker.add("config", "Setup configuration file") + tracker.complete("config", "created") + except Exception as e: + if tracker: + tracker.add("config", "Setup configuration file") + tracker.error("config", str(e)) + def ensure_executable_scripts(project_path: Path, tracker: StepTracker | None = None) -> None: """Ensure POSIX .sh scripts under .specify/scripts (recursively) have execute bits (no-op on Windows).""" if os.name == "nt": @@ -1046,6 +1063,7 @@ def init( download_and_extract_template(project_path, selected_ai, selected_script, here, verbose=False, tracker=tracker, client=local_client, debug=debug, github_token=github_token) + setup_config_file(project_path, tracker=tracker) ensure_executable_scripts(project_path, tracker=tracker) if not no_git: diff --git a/templates/commands/specify.md b/templates/commands/specify.md index 69258a8bf..8a0e18fc0 100644 --- a/templates/commands/specify.md +++ b/templates/commands/specify.md @@ -13,6 +13,152 @@ $ARGUMENTS You **MUST** consider the user input before proceeding (if not empty). +## Spec Number Option + +Users can optionally specify a custom spec number when creating a feature by including it in their command. This is particularly useful for matching issue tracker numbers (GitHub issues, Jira tickets, etc.). + +**How to recognize spec number in user input:** + +- `--number ` or `-SpecNumber ` format +- Keywords like "issue #42", "ticket 123", "for issue 1234" +- Direct number references: "spec 42", "number 99" +- **Natural language patterns combining prefix and number:** + + **Detection algorithm (use in order):** + + 1. **First, look for adjacent prefix + number** (most common): + - Pattern: `[prefix_keyword] [number]` appearing together + - Examples: "feature 303", "bugfix 666", "hotfix 42", "fix 123" + - If found: Extract both prefix and number, done. + + 2. **If not found, scan the entire input more broadly**: + - Search **anywhere** in the input for prefix keywords: + - "feature" or "features" → `feature/` + - "bugfix" or "bug fix" or "fix" → `bugfix/` + - "hotfix" or "hot fix" → `hotfix/` + - "chore" → `chore/` + - "refactor" or "refactoring" → `refactor/` + - Search **anywhere** in the input for number patterns: "#221", "221", "issue 221", "ticket 221", "spec 221", "number 221" + - If BOTH prefix keyword AND number found: Combine them + - If only number found: Extract just the number (auto-prefix from config) + - If only prefix keyword found: Ignore (not enough information) + + 3. **Handle conflicts** (if multiple prefix keywords found): + - Use the keyword that appears closest to the number + - If equidistant, prefer more specific: "bugfix" > "fix" + - If still tied, use first occurrence (left to right) + + **This handles all these patterns:** + - "feature 303 add cart" ✓ (adjacent) + - "This is feature 221" ✓ (adjacent within sentence) + - "For issue #221, make it a feature" ✓ (separated, closest keyword) + - "#221 feature" ✓ (separated, number first) + - "issue #221" ✓ (just number) + - "Add shopping cart feature 303" ✓ (adjacent but later in sentence) + +**Examples of user input with spec number:** + +- "Add user authentication --number 42" (explicit parameter) +- "Fix login timeout for issue #123" (extract `123` only) +- "Implement payment API as spec 1234" (extract `1234` only) +- "Add search feature --number 99 --branch-prefix feature/" (explicit parameters) +- "feature 303 add shopping cart" (extract `feature/` and `303` - adjacent pattern) +- "bugfix 666 fix payment timeout" (extract `bugfix/` and `666` - adjacent pattern) +- "This is feature 221" (extract `feature/` and `221` - adjacent pattern in sentence) +- "For issue #221, make it a feature" (extract `feature/` and `221` - separated, keyword closest to number) +- "Add hotfix 42 for critical bug" (extract `hotfix/` and `42` - adjacent pattern) +- "#999 chore cleanup old files" (extract `chore/` and `999` - number first, then keyword) + +**If spec number is specified:** + +1. **Scan and extract** using the detection algorithm above: + - Look for adjacent patterns first (e.g., "feature 303") + - If not found, scan entire input for separated keywords and numbers + - Extract both prefix type and number if found together + +2. **Process extracted values:** + - Normalize "fix" to "bugfix/" for consistency + - Normalize "bug fix" to "bugfix/" for consistency + - Normalize "hot fix" to "hotfix/" for consistency + - Add trailing slash to create proper prefix (e.g., "feature" → "feature/") + - Validate the number is a positive integer + +3. **Clean the feature description:** + - Remove the spec number from the description (e.g., "221" or "#221" or "issue 221") + - Remove the prefix keyword if it was used as a branch type indicator (e.g., remove "feature" from "This is feature 221" to get "This is") + - Clean up any resulting double spaces or hanging prepositions + +4. **Pass to script:** + - Bash: `--number 42` and optionally `--branch-prefix "feature/"` + - PowerShell: `-SpecNumber 42` and optionally `-BranchPrefix "feature/"` + +**If no spec number is specified:** The script will auto-increment from the highest existing spec number (default behavior). + +**Priority order:** +1. `--number` CLI parameter (highest priority) +2. `SPECIFY_SPEC_NUMBER` environment variable +3. Auto-increment (default) + +**Recognized prefix types for natural language patterns:** +- `feature` or `features` → `feature/` +- `bugfix` or `bug fix` → `bugfix/` +- `fix` → `bugfix/` (normalized, lower priority if "bugfix" also present) +- `hotfix` or `hot fix` → `hotfix/` +- `chore` → `chore/` +- `refactor` or `refactoring` → `refactor/` + +**Key principle:** Scan the ENTIRE user input for these keywords and numbers. They don't need to be adjacent or in any particular order. The algorithm will find them wherever they appear. + +## Branch Prefix Option + +Users can optionally specify a branch prefix when creating a feature by including it in their command. Look for these patterns in the user input: + +- `--branch-prefix ` or `-BranchPrefix ` format +- Keywords like "use prefix", "with prefix", "as a feature branch", "as a bugfix", etc. +- **Natural language patterns** (also extracts spec number if present): + - Scan the entire input for prefix keywords: "feature", "bugfix", "hotfix", "chore", "refactor" + - These keywords can appear anywhere in the sentence, not just adjacent to a number + - Examples: + - "feature 303" → prefix `feature/` and number `303` (adjacent) + - "This is feature 221" → prefix `feature/` and number `221` (adjacent in sentence) + - "bugfix 666 fix timeout" → prefix `bugfix/` and number `666` (adjacent) + - "For issue #42, make it a hotfix" → prefix `hotfix/` and number `42` (separated) + - "#999 chore task" → prefix `chore/` and number `999` (number first) + + **Key:** The reference to prefix and number may come anywhere in the prompt - scan the entire input. + +**Common prefix patterns:** + +- `feature/` - For feature branches +- `bugfix/` or `fix/` - For bug fixes +- `hotfix/` - For urgent production fixes +- `refactor/` - For refactoring work +- `chore/` - For maintenance tasks + +**Examples of user input with branch prefix:** + +- "Add user authentication --branch-prefix feature/" (explicit parameter) +- "Fix login timeout as a bugfix" (infer `bugfix/` prefix from keyword) +- "Update payment API with prefix hotfix/" (explicit mention of prefix) +- "feature 303 implement shopping cart" (extract `feature/` and `303` - adjacent) +- "This is feature 221 for auth" (extract `feature/` and `221` - adjacent in sentence) +- "bugfix 666 resolve payment issue" (extract `bugfix/` and `666` - adjacent) +- "For issue #42, create hotfix branch" (extract `hotfix/` and `42` - separated) +- "Make #100 a chore task" (extract `chore/` and `100` - separated) + +**If branch prefix is specified:** + +1. Extract the prefix from the user input +2. **If using natural language pattern** (e.g., "feature 303"): + - The spec number will also be extracted (see "Spec Number Option" above) + - Both prefix and number are removed from the feature description before processing +3. Remove the prefix specification from the feature description before processing +4. Pass the prefix to the script using the appropriate parameter: + - Bash: `--branch-prefix "prefix-value"` + - PowerShell: `-BranchPrefix "prefix-value"` + +**If no prefix is specified:** The script will use the default from configuration (`.specify/config.json`) or environment variable. + ## Outline The text the user typed after `/speckit.specify` in the triggering message **is** the feature description. Assume you always have it available in this conversation even if `{ARGS}` appears literally below. Do not ask the user to repeat it unless they provided an empty command. @@ -51,16 +197,35 @@ Given that feature description, do this: 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 - 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 specified (see "Spec Number Option" above), include it as a parameter + - If a branch prefix was specified (see "Branch Prefix Option" above), 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 + - Bash examples: + - `--short-name "your-generated-short-name" "Feature description here"` + - `--short-name "user-auth" "Add user authentication"` + - `--number 42 --short-name "payment-api" "Add payment processing"` + - `--number 1234 --short-name "user-auth" --branch-prefix "feature/" "Add user authentication"` + - `--number 303 --branch-prefix "feature/" --short-name "shopping-cart" "Add shopping cart"` (from "feature 303 add shopping cart") + - `--number 666 --branch-prefix "bugfix/" --short-name "payment-timeout" "Fix payment timeout"` (from "bugfix 666 fix payment timeout") + - PowerShell examples: + - `-ShortName "your-generated-short-name" "Feature description here"` + - `-ShortName "user-auth" "Add user authentication"` + - `-SpecNumber 42 -ShortName "payment-api" "Add payment processing"` + - `-SpecNumber 1234 -ShortName "user-auth" -BranchPrefix "feature/" "Add user authentication"` + - `-SpecNumber 303 -BranchPrefix "feature/" -ShortName "shopping-cart" "Add shopping cart"` (from "feature 303 add shopping cart") + - `-SpecNumber 666 -BranchPrefix "bugfix/" -ShortName "payment-timeout" "Fix payment timeout"` (from "bugfix 666 fix payment timeout") - 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 3. Load `templates/spec-template.md` to understand required sections. diff --git a/templates/config.json b/templates/config.json new file mode 100644 index 000000000..a0da0c0a4 --- /dev/null +++ b/templates/config.json @@ -0,0 +1,5 @@ +{ + "branch": { + "prefix": "" + } +}