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

### 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`
Comment on lines +17 to +24
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.
- 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/`)


## [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
92 changes: 92 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,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.<br/>**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

Expand Down Expand Up @@ -393,6 +457,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
Expand All @@ -414,6 +480,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/`
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.
- Git branch: `bugfix/123-fix-payment-timeout`

**Common workflows:**

- **GitHub Issues:** `--number <issue-number>` (e.g., `--number 456`)
- **Jira Tickets:** `--number <ticket-id>` (e.g., `--number 789` for PROJ-789)
- **Linear Issues:** `--number <issue-id>` (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.
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
37 changes: 24 additions & 13 deletions scripts/bash/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -85,28 +90,34 @@ 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
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)
Comment on lines +113 to +120
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.
fi

# Handle results
Expand All @@ -118,7 +129,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
Expand Down
86 changes: 76 additions & 10 deletions scripts/bash/create-new-feature.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ set -e

JSON_MODE=false
SHORT_NAME=""
BRANCH_PREFIX_ARG=""
BRANCH_NUMBER=""
ARGS=()
i=1
Expand All @@ -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
Expand All @@ -41,17 +56,20 @@ while [ $i -le $# ]; do
BRANCH_NUMBER="$next_arg"
;;
--help|-h)
echo "Usage: $0 [--json] [--short-name <name>] [--number N] <feature_description>"
echo "Usage: $0 [--json] [--short-name <name>] [--branch-prefix <prefix>] [--number N] <feature_description>"
echo ""
echo "Options:"
echo " --json Output in JSON format"
echo " --short-name <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 <name> Provide a custom short name (2-4 words) for the branch"
echo " --branch-prefix <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
;;
*)
Expand Down Expand Up @@ -245,7 +263,10 @@ fi

# Determine branch number
if [ -z "$BRANCH_NUMBER" ]; then
if [ "$HAS_GIT" = true ]; then
# Check environment variable first
if [ -n "$SPECIFY_SPEC_NUMBER" ]; then
BRANCH_NUMBER="$SPECIFY_SPEC_NUMBER"
elif [ "$HAS_GIT" = true ]; then
# Check existing branches on remotes
BRANCH_NUMBER=$(check_existing_branches "$BRANCH_SUFFIX" "$SPECS_DIR")
else
Expand All @@ -256,23 +277,68 @@ 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/')
Comment on lines +297 to +298
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.
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}
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.
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.
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)
# 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}"
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)"
Expand Down
Loading
Loading