diff --git a/scripts/bash/create-new-feature.sh b/scripts/bash/create-new-feature.sh index 5cb17fabe..60d37d192 100644 --- a/scripts/bash/create-new-feature.sh +++ b/scripts/bash/create-new-feature.sh @@ -3,18 +3,47 @@ set -e JSON_MODE=false +FEATURE_NAME="" ARGS=() -for arg in "$@"; do - case "$arg" in - --json) JSON_MODE=true ;; - --help|-h) echo "Usage: $0 [--json] "; exit 0 ;; - *) ARGS+=("$arg") ;; + +# Parse arguments +while [[ $# -gt 0 ]]; do + case "$1" in + --json) + JSON_MODE=true + shift + ;; + --feature-name=*) + FEATURE_NAME="${1#*=}" + shift + ;; + --feature-name) + if [[ -n "$2" && "$2" != --* ]]; then + FEATURE_NAME="$2" + shift 2 + else + echo "Error: --feature-name requires a value" >&2 + exit 1 + fi + ;; + --help|-h) + echo "Usage: $0 [--json] [--feature-name=] " + exit 0 + ;; + -*) + echo "Unknown option: $1" >&2 + exit 1 + ;; + *) + ARGS+=("$1") + shift + ;; esac done FEATURE_DESCRIPTION="${ARGS[*]}" if [ -z "$FEATURE_DESCRIPTION" ]; then - echo "Usage: $0 [--json] " >&2 + echo "Usage: $0 [--json] [--feature-name=] " >&2 exit 1 fi @@ -31,6 +60,34 @@ find_repo_root() { return 1 } +# Normalise AI-provided feature or description text into a kebab-case slug limited by an optional word cap. +# Assumes input contains valid alphanumeric characters. WordLimit of 0 removes the cap. +get_feature_slug() { + local raw="$1" + local word_limit="${2:-3}" + + # Convert to lowercase and replace non-alphanumeric with dashes + local slug=$(echo "$raw" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//') + + # Split into words and apply word limit + if [ "$word_limit" -le 0 ]; then + echo "$slug" + else + local words=() + IFS='-' read -ra ADDR <<< "$slug" + local count=0 + for word in "${ADDR[@]}"; do + [ -n "$word" ] || continue + words+=("$word") + count=$((count + 1)) + if [ "$count" -ge "$word_limit" ]; then + break + fi + done + IFS='-'; echo "${words[*]}" + fi +} + # Resolve repository root. Prefer git information when available, but fall back # to searching for repository markers so the workflow still functions in repositories that # were initialised with --no-git. @@ -67,9 +124,14 @@ fi NEXT=$((HIGHEST + 1)) FEATURE_NUM=$(printf "%03d" "$NEXT") -BRANCH_NAME=$(echo "$FEATURE_DESCRIPTION" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//') -WORDS=$(echo "$BRANCH_NAME" | tr '-' '\n' | grep -v '^$' | head -3 | tr '\n' '-' | sed 's/-$//') -BRANCH_NAME="${FEATURE_NUM}-${WORDS}" +# Generate slug for branch naming from AI-provided text: prioritize explicit feature name over description +if [ -n "$FEATURE_NAME" ]; then + SELECTED_SLUG=$(get_feature_slug "$FEATURE_NAME" 0) +else + SELECTED_SLUG=$(get_feature_slug "$FEATURE_DESCRIPTION" 3) +fi + +BRANCH_NAME="${FEATURE_NUM}-${SELECTED_SLUG}" if [ "$HAS_GIT" = true ]; then git checkout -b "$BRANCH_NAME" @@ -88,10 +150,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","HAS_GIT":%s}\n' "$BRANCH_NAME" "$SPEC_FILE" "$FEATURE_NUM" "$([ "$HAS_GIT" = true ] && echo 'true' || echo 'false')" else echo "BRANCH_NAME: $BRANCH_NAME" echo "SPEC_FILE: $SPEC_FILE" echo "FEATURE_NUM: $FEATURE_NUM" + echo "HAS_GIT: $HAS_GIT" echo "SPECIFY_FEATURE environment variable set to: $BRANCH_NAME" fi diff --git a/scripts/powershell/create-new-feature.ps1 b/scripts/powershell/create-new-feature.ps1 index 0f1f59127..193e0b241 100644 --- a/scripts/powershell/create-new-feature.ps1 +++ b/scripts/powershell/create-new-feature.ps1 @@ -3,6 +3,7 @@ [CmdletBinding()] param( [switch]$Json, + [string]$FeatureName, [Parameter(ValueFromRemainingArguments = $true)] [string[]]$FeatureDescription ) @@ -13,6 +14,7 @@ if (-not $FeatureDescription -or $FeatureDescription.Count -eq 0) { exit 1 } $featureDesc = ($FeatureDescription -join ' ').Trim() +$explicitFeatureName = if ($null -ne $FeatureName) { $FeatureName.Trim() } else { $null } # Resolve repository root. Prefer git information when available, but fall back # to searching for repository markers so the workflow still functions in repositories that @@ -37,6 +39,31 @@ function Find-RepositoryRoot { $current = $parent } } + +# Normalise AI-provided feature or description text into a kebab-case slug limited by an optional word cap. +# Assumes input contains valid alphanumeric characters. WordLimit of 0 removes the cap. +function Get-FeatureSlug { + param( + [string]$Raw, + [int]$WordLimit = 3 + ) + + $slug = $Raw.ToLower() + $slug = $slug -replace '[^a-z0-9]', '-' -replace '-{2,}', '-' -replace '^-', '' -replace '-$', '' + + $parts = ($slug -split '-') | Where-Object { $_ } + + if ($WordLimit -le 0) { + return [string]::Join('-', $parts) + } + + if ($parts.Count -gt $WordLimit) { + $parts = $parts[0..($WordLimit - 1)] + } + + return [string]::Join('-', $parts) +} + $fallbackRoot = (Find-RepositoryRoot -StartDir $PSScriptRoot) if (-not $fallbackRoot) { Write-Error "Error: Could not determine repository root. Please run this script from within the repository." @@ -72,9 +99,15 @@ if (Test-Path $specsDir) { $next = $highest + 1 $featureNum = ('{0:000}' -f $next) -$branchName = $featureDesc.ToLower() -replace '[^a-z0-9]', '-' -replace '-{2,}', '-' -replace '^-', '' -replace '-$', '' -$words = ($branchName -split '-') | Where-Object { $_ } | Select-Object -First 3 -$branchName = "$featureNum-$([string]::Join('-', $words))" +# Generate slug for branch naming from AI-provided text: prioritize explicit feature name over description +if (-not [string]::IsNullOrEmpty($explicitFeatureName)) { + $selectedSlug = Get-FeatureSlug -Raw $explicitFeatureName -WordLimit 0 +} +else { + $selectedSlug = Get-FeatureSlug -Raw $featureDesc -WordLimit 3 +} + +$branchName = "$featureNum-$selectedSlug" if ($hasGit) { try { diff --git a/templates/commands/specify.md b/templates/commands/specify.md index 652c86a27..5648b630c 100644 --- a/templates/commands/specify.md +++ b/templates/commands/specify.md @@ -11,14 +11,21 @@ User input: $ARGUMENTS -The text the user typed after `/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. +The text the user typed after `/specify` in the triggering message **is** the feature description. Assume you always have it available in this conversation even if `$ARGUMENTS` appears literally below. Do not ask the user to repeat it unless they provided an empty command. Given that feature description, do this: -1. Run the script `{SCRIPT}` from repo root and parse its JSON output for BRANCH_NAME and SPEC_FILE. All file paths must be absolute. - **IMPORTANT** You must only ever run this script once. The JSON is provided in the terminal as output - always refer to it to get the actual content you're looking for. -2. Load `templates/spec-template.md` to understand required sections. -3. Write the specification to SPEC_FILE using the template structure, replacing placeholders with concrete details derived from the feature description (arguments) while preserving section order and headings. -4. Report completion with branch name, spec file path, and readiness for the next phase. +1. Inspect the user input for an explicit feature name directive. Treat any line matching (case-insensitive) `feature-name:`, `feature name:`, `branch-name:`, or `branch name:` as providing the feature name; trim the value. Remove that directive line from the text you consider the feature description. If the trimmed value is empty, treat it as though no directive was supplied. +2. If no explicit name was provided, synthesize a concise feature name from the description before continuing: + - Summarize the feature into a short 3–5 word phrase (Title Case, human readable). + - Avoid punctuation except spaces and basic hyphens. + - Use that generated phrase when passing `-FeatureName` in the next step. +3. From repo root, run the script `.specify/scripts/powershell/create-new-feature.ps1 -Json` **exactly once**: + - Pass the cleaned feature description as positional arguments. + - Always include `-FeatureName ""` where `` is either the explicit directive or the synthesized name. + - Parse the JSON output for `BRANCH_NAME` and `SPEC_FILE`. All file paths must be absolute. +4. Load `.specify/templates/spec-template.md` to understand required sections. +5. Write the specification to SPEC_FILE using the template structure, replacing placeholders with concrete details derived from the feature description while preserving section order and headings. +6. Report completion with branch name, spec file path, and readiness for the next phase. Note: The script creates and checks out the new branch and initializes the spec file before writing.