diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 963bc4bf6e..84086cf3a2 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -70,3 +70,4 @@ Closes - [ ] [Changelog entry](../dev/guidelines/changelog.md) added (`uv run towncrier create ...`) - [ ] External docs updated (if user-facing or ops-facing change) - [ ] Internal .md docs updated (internal knowledge and AI code tools knowledge) +- [ ] I have reviewed AI generated content diff --git a/.specify/scripts/bash/common.sh b/.specify/scripts/bash/common.sh index 2c3165e41d..2a555588f1 100755 --- a/.specify/scripts/bash/common.sh +++ b/.specify/scripts/bash/common.sh @@ -37,8 +37,8 @@ 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]} + if [[ "$dirname" =~ ^([a-z]{2,4}-)?([0-9]{3})- ]]; then + local number=${BASH_REMATCH[2]} number=$((10#$number)) if [[ "$number" -gt "$highest" ]]; then highest=$number @@ -72,9 +72,9 @@ check_feature_branch() { return 0 fi - if [[ ! "$branch" =~ ^[0-9]{3}- ]]; then + if [[ ! "$branch" =~ ^([a-z]{2,4}-)?[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: fac-001-feature-name (initials-number-name)" >&2 return 1 fi @@ -84,27 +84,30 @@ check_feature_branch() { 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) +# This allows multiple branches to work on the same spec (e.g., fac-004-fix-bug, jdo-004-add-feature) 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 "fac-004-whatever" or "004-whatever") + if [[ ! "$branch_name" =~ ^(([a-z]{2,4})-)?([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 prefix="${BASH_REMATCH[3]}" - # Search for directories in specs/ that start with this prefix + # Search for directories in specs/ that match this numeric prefix (any initials) local matches=() if [[ -d "$specs_dir" ]]; then - for dir in "$specs_dir"/"$prefix"-*; do + for dir in "$specs_dir"/*; do if [[ -d "$dir" ]]; then - matches+=("$(basename "$dir")") + local dirname=$(basename "$dir") + if [[ "$dirname" =~ ^([a-z]{2,4}-)?${prefix}- ]]; then + matches+=("$dirname") + fi fi done fi diff --git a/.specify/scripts/bash/create-new-feature.sh b/.specify/scripts/bash/create-new-feature.sh index c40cfd77f0..76b64861ac 100755 --- a/.specify/scripts/bash/create-new-feature.sh +++ b/.specify/scripts/bash/create-new-feature.sh @@ -5,6 +5,7 @@ set -e JSON_MODE=false SHORT_NAME="" BRANCH_NUMBER="" +INITIALS="" ARGS=() i=1 while [ $i -le $# ]; do @@ -40,18 +41,32 @@ while [ $i -le $# ]; do fi BRANCH_NUMBER="$next_arg" ;; - --help|-h) - echo "Usage: $0 [--json] [--short-name ] [--number N] " + --initials) + if [ $((i + 1)) -gt $# ]; then + echo 'Error: --initials requires a value' >&2 + exit 1 + fi + i=$((i + 1)) + next_arg="${!i}" + if [[ "$next_arg" == --* ]]; then + echo 'Error: --initials requires a value' >&2 + exit 1 + fi + INITIALS="$next_arg" + ;; + --help|-h) + echo "Usage: $0 [--json] [--short-name ] [--number N] [--initials ] " 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 " --initials Specify initials manually (overrides OPSMILL_GIT_USER_SHORT and git user.name)" 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 'Implement OAuth2 integration for API' --number 5 --initials fac" exit 0 ;; *) @@ -63,7 +78,7 @@ done FEATURE_DESCRIPTION="${ARGS[*]}" if [ -z "$FEATURE_DESCRIPTION" ]; then - echo "Usage: $0 [--json] [--short-name ] [--number N] " >&2 + echo "Usage: $0 [--json] [--short-name ] [--number N] [--initials ] " >&2 exit 1 fi @@ -80,6 +95,44 @@ find_repo_root() { return 1 } +# Function to extract user short name / initials. +# Checks OPSMILL_GIT_USER_SHORT env var first, then falls back to deriving +# initials from git user.name (first letter of first name + first two letters of last name). +# Example: "Fatih Acar" -> "fac", "John Doe" -> "jdo" +get_git_initials() { + # Prefer explicit environment variable + if [ -n "${OPSMILL_GIT_USER_SHORT:-}" ]; then + echo "$OPSMILL_GIT_USER_SHORT" | tr '[:upper:]' '[:lower:]' + return 0 + fi + + local full_name + full_name=$(git config user.name 2>/dev/null || echo "") + + if [ -z "$full_name" ]; then + echo "" + return 1 + fi + + # Split into words, take first and last + local first_name last_name + first_name=$(echo "$full_name" | awk '{print $1}') + last_name=$(echo "$full_name" | awk '{print $NF}') + + # If only one name provided, use first 3 chars of that name + if [ "$first_name" = "$last_name" ]; then + echo "$full_name" | tr '[:upper:]' '[:lower:]' | cut -c1-3 + return 0 + fi + + # First letter of first name + first two letters of last name + local first_initial last_initials + first_initial=$(echo "$first_name" | tr '[:upper:]' '[:lower:]' | cut -c1) + last_initials=$(echo "$last_name" | tr '[:upper:]' '[:lower:]' | cut -c1-2) + + echo "${first_initial}${last_initials}" +} + # Function to get highest number from specs directory get_highest_from_specs() { local specs_dir="$1" @@ -89,7 +142,8 @@ get_highest_from_specs() { for dir in "$specs_dir"/*; do [ -d "$dir" ] || continue dirname=$(basename "$dir") - number=$(echo "$dirname" | grep -o '^[0-9]\+' || echo "0") + # Strip optional initials prefix (2-4 lowercase letters followed by hyphen) + number=$(echo "$dirname" | sed 's/^[a-z]\{2,4\}-//' | grep -o '^[0-9]\+' || echo "0") number=$((10#$number)) if [ "$number" -gt "$highest" ]; then highest=$number @@ -112,8 +166,14 @@ get_highest_from_branches() { # Clean branch name: remove leading markers and remote prefixes clean_branch=$(echo "$branch" | sed 's/^[* ]*//; s|^remotes/[^/]*/||') - # Extract feature number if branch matches pattern ###-* - if echo "$clean_branch" | grep -q '^[0-9]\{3\}-'; then + # Extract feature number if branch matches new format: initials-###-* or old format: ###-* + if echo "$clean_branch" | grep -q '^[a-z]\{2,4\}-[0-9]\{3\}-'; then + number=$(echo "$clean_branch" | sed 's/^[a-z]*-//' | grep -o '^[0-9]\{3\}' || echo "0") + number=$((10#$number)) + if [ "$number" -gt "$highest" ]; then + highest=$number + fi + elif echo "$clean_branch" | grep -q '^[0-9]\{3\}-'; then number=$(echo "$clean_branch" | grep -o '^[0-9]\{3\}' || echo "0") number=$((10#$number)) if [ "$number" -gt "$highest" ]; then @@ -246,26 +306,43 @@ if [ -z "$BRANCH_NUMBER" ]; then fi fi +# Determine initials +if [ -z "$INITIALS" ]; then + if [ "$HAS_GIT" = true ]; then + INITIALS=$(get_git_initials) + if [ -z "$INITIALS" ]; then + echo "Error: Could not determine initials." >&2 + echo "Set OPSMILL_GIT_USER_SHORT, git config user.name, or use --initials " >&2 + exit 1 + fi + else + echo "Error: No git available and --initials not provided." >&2 + echo "Use --initials to specify manually." >&2 + exit 1 + fi +fi + # Force base-10 interpretation to prevent octal conversion (e.g., 010 → 8 in octal, but should be 10 in decimal) FEATURE_NUM=$(printf "%03d" "$((10#$BRANCH_NUMBER))") -BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}" +BRANCH_NAME="${INITIALS}-${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)) - + # Account for: initials (variable) + hyphen (1) + feature number (3) + hyphen (1) + PREFIX_LENGTH=$(( ${#INITIALS} + 1 + 3 + 1 )) + MAX_SUFFIX_LENGTH=$((MAX_BRANCH_LENGTH - PREFIX_LENGTH)) + # 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}" - + BRANCH_NAME="${INITIALS}-${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)" @@ -288,10 +365,11 @@ if [ -f "$TEMPLATE" ]; then cp "$TEMPLATE" "$SPEC_FILE"; else touch "$SPEC_FILE" export SPECIFY_FEATURE="$BRANCH_NAME" if $JSON_MODE; then - printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s"}\n' "$BRANCH_NAME" "$SPEC_FILE" "$FEATURE_NUM" + printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s","INITIALS":"%s"}\n' "$BRANCH_NAME" "$SPEC_FILE" "$FEATURE_NUM" "$INITIALS" else echo "BRANCH_NAME: $BRANCH_NAME" echo "SPEC_FILE: $SPEC_FILE" echo "FEATURE_NUM: $FEATURE_NUM" + echo "INITIALS: $INITIALS" echo "SPECIFY_FEATURE environment variable set to: $BRANCH_NAME" fi diff --git a/.specify/templates/plan-template.md b/.specify/templates/plan-template.md index 6a8bfc6c8a..d516b5f432 100644 --- a/.specify/templates/plan-template.md +++ b/.specify/templates/plan-template.md @@ -1,7 +1,7 @@ # Implementation Plan: [FEATURE] -**Branch**: `[###-feature-name]` | **Date**: [DATE] | **Spec**: [link] -**Input**: Feature specification from `/specs/[###-feature-name]/spec.md` +**Branch**: `[initials-###-feature-name]` | **Date**: [DATE] | **Spec**: [link] +**Input**: Feature specification from `/specs/[initials-###-feature-name]/spec.md` **Note**: This template is filled in by the `/speckit.plan` command. See `.specify/templates/commands/plan.md` for the execution workflow. @@ -38,7 +38,7 @@ ### Documentation (this feature) ```text -specs/[###-feature]/ +specs/[initials-###-feature]/ ├── plan.md # This file (/speckit.plan command output) ├── research.md # Phase 0 output (/speckit.plan command) ├── data-model.md # Phase 1 output (/speckit.plan command) diff --git a/.specify/templates/spec-template.md b/.specify/templates/spec-template.md index c67d914980..b563b5309e 100644 --- a/.specify/templates/spec-template.md +++ b/.specify/templates/spec-template.md @@ -1,6 +1,6 @@ # Feature Specification: [FEATURE NAME] -**Feature Branch**: `[###-feature-name]` +**Feature Branch**: `[initials-###-feature-name]` **Created**: [DATE] **Status**: Draft **Input**: User description: "$ARGUMENTS" diff --git a/.specify/templates/tasks-template.md b/.specify/templates/tasks-template.md index 60f9be455d..5264c07ae5 100644 --- a/.specify/templates/tasks-template.md +++ b/.specify/templates/tasks-template.md @@ -5,7 +5,7 @@ description: "Task list template for feature implementation" # Tasks: [FEATURE NAME] -**Input**: Design documents from `/specs/[###-feature-name]/` +**Input**: Design documents from `/specs/[initials-###-feature-name]/` **Prerequisites**: plan.md (required), spec.md (required for user stories), research.md, data-model.md, contracts/ **Tests**: The examples below include test tasks. Tests are OPTIONAL - only include them if explicitly requested in the feature specification. diff --git a/dev/commands/speckit.specify.md b/dev/commands/speckit.specify.md index 49abdcb778..7c8d29ff06 100644 --- a/dev/commands/speckit.specify.md +++ b/dev/commands/speckit.specify.md @@ -45,9 +45,9 @@ Given that feature description, do this: ``` b. Find the highest feature number across all sources for the short-name: - - Remote branches: `git ls-remote --heads origin | grep -E 'refs/heads/[0-9]+-$'` - - Local branches: `git branch | grep -E '^[* ]*[0-9]+-$'` - - Specs directories: Check for directories matching `specs/[0-9]+-` + - Remote branches: `git ls-remote --heads origin | grep -E 'refs/heads/([a-z]{2,4}-)?[0-9]+-$'` + - Local branches: `git branch | grep -E '^[* ]*([a-z]{2,4}-)?[0-9]+-$'` + - Specs directories: Check for directories matching `specs/([a-z]{2,4}-)?[0-9]+-` c. Determine the next available number: - Extract all numbers from all three sources @@ -56,11 +56,13 @@ Given that feature description, do this: d. Run the script `.specify/scripts/bash/create-new-feature.sh --json "$ARGUMENTS"` with the calculated number and short-name: - Pass `--number N+1` and `--short-name "your-short-name"` along with the feature description - - Bash example: `.specify/scripts/bash/create-new-feature.sh --json "$ARGUMENTS" --json --number 5 --short-name "user-auth" "Add user authentication"` - - PowerShell example: `.specify/scripts/bash/create-new-feature.sh --json "$ARGUMENTS" -Json -Number 5 -ShortName "user-auth" "Add user authentication"` + - The `--initials` flag is optional; initials are auto-detected from `git config user.name` (first letter of first name + first two letters of last name, lowercased) + - Bash example: `.specify/scripts/bash/create-new-feature.sh --json --number 5 --short-name "user-auth" "Add user authentication"` + - PowerShell example: `.specify/scripts/bash/create-new-feature.sh --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 + - Numbers are global across all developers -- scan ALL branches regardless of initials prefix - 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