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
1 change: 1 addition & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,4 @@ Closes <!-- #issue -->
- [ ] [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
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix grammar: hyphenate "AI-generated".

When "AI generated" is used as a compound adjective modifying "content", it should be hyphenated.

📝 Proposed fix
-- [ ] I have reviewed AI generated content
+- [ ] I have reviewed AI-generated content
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- [ ] I have reviewed AI generated content
- [ ] I have reviewed AI-generated content
🧰 Tools
🪛 LanguageTool

[grammar] ~73-~73: Use a hyphen to join words.
Context: ...ools knowledge) - [ ] I have reviewed AI generated content

(QB_NEW_EN_HYPHEN)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/pull_request_template.md at line 73, Update the checklist item text
"I have reviewed AI generated content" to hyphenate the compound adjective by
changing it to "I have reviewed AI-generated content"; locate and edit the
checkbox line in the pull request template (the string "I have reviewed AI
generated content") and replace it with the hyphenated form to fix the grammar.

25 changes: 14 additions & 11 deletions .specify/scripts/bash/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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
Expand Down
108 changes: 93 additions & 15 deletions .specify/scripts/bash/create-new-feature.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ set -e
JSON_MODE=false
SHORT_NAME=""
BRANCH_NUMBER=""
INITIALS=""
ARGS=()
i=1
while [ $i -le $# ]; do
Expand Down Expand Up @@ -40,18 +41,32 @@ while [ $i -le $# ]; do
fi
BRANCH_NUMBER="$next_arg"
;;
--help|-h)
echo "Usage: $0 [--json] [--short-name <name>] [--number N] <feature_description>"
--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 <name>] [--number N] [--initials <init>] <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 " --initials <init> 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
;;
*)
Expand All @@ -63,7 +78,7 @@ done

FEATURE_DESCRIPTION="${ARGS[*]}"
if [ -z "$FEATURE_DESCRIPTION" ]; then
echo "Usage: $0 [--json] [--short-name <name>] [--number N] <feature_description>" >&2
echo "Usage: $0 [--json] [--short-name <name>] [--number N] [--initials <init>] <feature_description>" >&2
exit 1
fi

Expand All @@ -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"
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 <initials>" >&2
exit 1
fi
else
echo "Error: No git available and --initials not provided." >&2
echo "Use --initials <initials> to specify manually." >&2
exit 1
fi
fi
Comment on lines +309 to +323
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing validation of INITIALS against the expected [a-z]{2,4} pattern.

INITIALS is used unchecked from three sources (--initials, OPSMILL_GIT_USER_SHORT, get_git_initials), none of which validate the final value. Invalid initials (e.g., containing digits, uppercase, special characters, or wrong length) will produce branch names that fail check_feature_branch validation later—and corrupt the printf-based JSON output on line 368 if they contain quotes.

Add a validation gate after INITIALS is determined:

📝 Proposed fix
     fi
 fi
 
+# Validate initials format
+if [[ ! "$INITIALS" =~ ^[a-z]{2,4}$ ]]; then
+    echo "Error: Initials '$INITIALS' must be 2-4 lowercase letters." >&2
+    echo "Use --initials <initials> to specify manually." >&2
+    exit 1
+fi
+
 # Force base-10 interpretation ...
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# 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 <initials>" >&2
exit 1
fi
else
echo "Error: No git available and --initials not provided." >&2
echo "Use --initials <initials> to specify manually." >&2
exit 1
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 <initials>" >&2
exit 1
fi
else
echo "Error: No git available and --initials not provided." >&2
echo "Use --initials <initials> to specify manually." >&2
exit 1
fi
fi
# Validate initials format
if [[ ! "$INITIALS" =~ ^[a-z]{2,4}$ ]]; then
echo "Error: Initials '$INITIALS' must be 2-4 lowercase letters." >&2
echo "Use --initials <initials> to specify manually." >&2
exit 1
fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.specify/scripts/bash/create-new-feature.sh around lines 309 - 323, After
INITIALS is set (from --initials, OPSMILL_GIT_USER_SHORT, or get_git_initials)
validate it against the required lowercase-letter pattern /^[a-z]{2,4}$/ before
proceeding: use a shell regex check (e.g. [[ "$INITIALS" =~ ^[a-z]{2,4}$ ]]) or
convert to lowercase then test, and if it fails emit a clear error and exit
non-zero; this prevents bad branch names used by check_feature_branch and avoids
corrupting the printf-based JSON output—ensure you reference and validate
INITIALS right after assignment and before any use in branch creation or printf
output.


# 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}"
Copy link
Contributor

Choose a reason for hiding this comment

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

We could perhaps get to this later, but if we're looking at changing the existing files it could also be good to mention that we can add a suffix key to link any PR to a Jira issue. I.e. if the branch name ends with something like "-IFC-906" we can then see that the issue is linked to a PR from with Jira.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah I also thought including Jira IDs could be good, that would require asking the user for the ID during /speckit.specify I guess
We can come back to this later?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes


# 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)"
Expand All @@ -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
6 changes: 3 additions & 3 deletions .specify/templates/plan-template.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion .specify/templates/spec-template.md
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
2 changes: 1 addition & 1 deletion .specify/templates/tasks-template.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
12 changes: 7 additions & 5 deletions dev/commands/speckit.specify.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]+-<short-name>$'`
- Local branches: `git branch | grep -E '^[* ]*[0-9]+-<short-name>$'`
- Specs directories: Check for directories matching `specs/[0-9]+-<short-name>`
- Remote branches: `git ls-remote --heads origin | grep -E 'refs/heads/([a-z]{2,4}-)?[0-9]+-<short-name>$'`
- Local branches: `git branch | grep -E '^[* ]*([a-z]{2,4}-)?[0-9]+-<short-name>$'`
- Specs directories: Check for directories matching `specs/([a-z]{2,4}-)?[0-9]+-<short-name>`

c. Determine the next available number:
- Extract all numbers from all three sources
Expand All @@ -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"`
Comment on lines +60 to +61
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

PowerShell example uses parameter names the bash script doesn't recognize.

Line 61 passes -Number and -ShortName (PowerShell cmdlet-style), but the bash script only parses --number and --short-name. An AI agent following this example will get an error or silently misinterpret the arguments.

Proposed fix
-      - PowerShell example: `.specify/scripts/bash/create-new-feature.sh --json -Number 5 -ShortName "user-auth" "Add user authentication"`
+      - PowerShell example: `.specify/scripts/bash/create-new-feature.sh --json --number 5 --short-name "user-auth" "Add user authentication"`
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- 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"`
- 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 --short-name "user-auth" "Add user authentication"`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dev/commands/speckit.specify.md` around lines 60 - 61, The PowerShell example
uses PowerShell-style parameters (-Number, -ShortName) that the bash script
.specify/scripts/bash/create-new-feature.sh does not recognize; update the
PowerShell example to pass the same long flags the script parses (use --number
and --short-name) so both examples use
`.specify/scripts/bash/create-new-feature.sh --json --number 5 --short-name
"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
Expand Down