Skip to content

Commit 30bf5c4

Browse files
committed
Add PROJECT_ACRONYM to branch naming and auto-fill constitution identity fields
1 parent 76cca34 commit 30bf5c4

File tree

9 files changed

+670
-70
lines changed

9 files changed

+670
-70
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,7 @@ docs/dev
5050
.specify/extensions/.cache/
5151
.specify/extensions/.backup/
5252
.specify/extensions/*/local-config.yml
53+
54+
55+
.claude/*
56+
.test/*

scripts/bash/common.sh

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,10 @@ get_current_branch() {
3737
for dir in "$specs_dir"/*; do
3838
if [[ -d "$dir" ]]; then
3939
local dirname=$(basename "$dir")
40-
if [[ "$dirname" =~ ^([0-9]{3})- ]]; then
40+
# Match both "001-name" and "ACR-001-name" directory patterns
41+
local stripped_dirname
42+
stripped_dirname=$(echo "$dirname" | sed 's/^[A-Z]\{2,5\}-//')
43+
if [[ "$stripped_dirname" =~ ^([0-9]{3})- ]]; then
4144
local number=${BASH_REMATCH[1]}
4245
number=$((10#$number))
4346
if [[ "$number" -gt "$highest" ]]; then
@@ -72,16 +75,16 @@ check_feature_branch() {
7275
return 0
7376
fi
7477

75-
if [[ ! "$branch" =~ ^[0-9]{3}- ]]; then
78+
if [[ ! "$branch" =~ ^(feature/([A-Z]+-)?)?[0-9]{3}- ]]; then
7679
echo "ERROR: Not on a feature branch. Current branch: $branch" >&2
77-
echo "Feature branches should be named like: 001-feature-name" >&2
80+
echo "Feature branches should be named like: feature/001-feature-name or feature/URA-001-feature-name" >&2
7881
return 1
7982
fi
8083

8184
return 0
8285
}
8386

84-
get_feature_dir() { echo "$1/specs/$2"; }
87+
get_feature_dir() { local dir="${2#feature/}"; echo "$1/specs/$dir"; }
8588

8689
# Find feature directory by numeric prefix instead of exact branch match
8790
# This allows multiple branches to work on the same spec (e.g., 004-fix-bug, 004-add-feature)
@@ -90,37 +93,49 @@ find_feature_dir_by_prefix() {
9093
local branch_name="$2"
9194
local specs_dir="$repo_root/specs"
9295

93-
# Extract numeric prefix from branch (e.g., "004" from "004-whatever")
94-
if [[ ! "$branch_name" =~ ^([0-9]{3})- ]]; then
96+
# Strip feature/ prefix for directory lookup
97+
local dir_name="${branch_name#feature/}"
98+
99+
# Extract numeric prefix from branch (e.g., "004" from "004-whatever" or "URA-004-whatever")
100+
local stripped_name
101+
stripped_name=$(echo "$dir_name" | sed 's/^[A-Z]\{2,5\}-//')
102+
if [[ ! "$stripped_name" =~ ^([0-9]{3})- ]]; then
95103
# If branch doesn't have numeric prefix, fall back to exact match
96-
echo "$specs_dir/$branch_name"
104+
echo "$specs_dir/$dir_name"
97105
return
98106
fi
99107

100108
local prefix="${BASH_REMATCH[1]}"
101109

102-
# Search for directories in specs/ that start with this prefix
110+
# Search for directories in specs/ that match this numeric prefix
111+
# Match both "001-name" and "ACR-001-name" patterns
103112
local matches=()
104113
if [[ -d "$specs_dir" ]]; then
105-
for dir in "$specs_dir"/"$prefix"-*; do
114+
for dir in "$specs_dir"/*; do
106115
if [[ -d "$dir" ]]; then
107-
matches+=("$(basename "$dir")")
116+
local base
117+
base=$(basename "$dir")
118+
local base_stripped
119+
base_stripped=$(echo "$base" | sed 's/^[A-Z]\{2,5\}-//')
120+
if [[ "$base_stripped" =~ ^${prefix}- ]]; then
121+
matches+=("$base")
122+
fi
108123
fi
109124
done
110125
fi
111126

112127
# Handle results
113128
if [[ ${#matches[@]} -eq 0 ]]; then
114-
# No match found - return the branch name path (will fail later with clear error)
115-
echo "$specs_dir/$branch_name"
129+
# No match found - return the dir name path (will fail later with clear error)
130+
echo "$specs_dir/$dir_name"
116131
elif [[ ${#matches[@]} -eq 1 ]]; then
117132
# Exactly one match - perfect!
118133
echo "$specs_dir/${matches[0]}"
119134
else
120135
# Multiple matches - this shouldn't happen with proper naming convention
121136
echo "ERROR: Multiple spec directories found with prefix '$prefix': ${matches[*]}" >&2
122137
echo "Please ensure only one spec directory exists per numeric prefix." >&2
123-
echo "$specs_dir/$branch_name" # Return something to avoid breaking the script
138+
echo "$specs_dir/$dir_name" # Return something to avoid breaking the script
124139
fi
125140
}
126141

scripts/bash/create-new-feature.sh

Lines changed: 124 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,10 @@ get_highest_from_specs() {
8989
for dir in "$specs_dir"/*; do
9090
[ -d "$dir" ] || continue
9191
dirname=$(basename "$dir")
92-
number=$(echo "$dirname" | grep -o '^[0-9]\+' || echo "0")
92+
# Strip optional acronym prefix (e.g., "URA-" from "URA-001-name")
93+
local stripped_dirname
94+
stripped_dirname=$(echo "$dirname" | sed 's/^[A-Z]\{2,5\}-//')
95+
number=$(echo "$stripped_dirname" | grep -o '^[0-9]\+' || echo "0")
9396
number=$((10#$number))
9497
if [ "$number" -gt "$highest" ]; then
9598
highest=$number
@@ -112,9 +115,15 @@ get_highest_from_branches() {
112115
# Clean branch name: remove leading markers and remote prefixes
113116
clean_branch=$(echo "$branch" | sed 's/^[* ]*//; s|^remotes/[^/]*/||')
114117

118+
# Strip feature/ prefix if present
119+
clean_branch="${clean_branch#feature/}"
120+
# Strip optional acronym prefix (e.g., "URA-" from "URA-001-name")
121+
local stripped_branch
122+
stripped_branch=$(echo "$clean_branch" | sed 's/^[A-Z]\{2,5\}-//')
123+
115124
# Extract feature number if branch matches pattern ###-*
116-
if echo "$clean_branch" | grep -q '^[0-9]\{3\}-'; then
117-
number=$(echo "$clean_branch" | grep -o '^[0-9]\{3\}' || echo "0")
125+
if echo "$stripped_branch" | grep -q '^[0-9]\{3\}-'; then
126+
number=$(echo "$stripped_branch" | grep -o '^[0-9]\{3\}' || echo "0")
118127
number=$((10#$number))
119128
if [ "$number" -gt "$highest" ]; then
120129
highest=$number
@@ -155,6 +164,60 @@ clean_branch_name() {
155164
echo "$name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//'
156165
}
157166

167+
# Function to extract project acronym from constitution.md
168+
get_project_acronym() {
169+
local repo_root="$1"
170+
local constitution="$repo_root/.specify/memory/constitution.md"
171+
172+
if [ ! -f "$constitution" ]; then
173+
echo ""
174+
return
175+
fi
176+
177+
# Try to extract project_acronym from YAML front matter
178+
local acronym=""
179+
if head -1 "$constitution" | grep -q '^---$'; then
180+
acronym=$(awk '/^---$/{n++; next} n==1 && /^project_acronym:/{sub(/^project_acronym:[[:space:]]*/,""); gsub(/^["'"'"']|["'"'"']$/,""); print; exit}' "$constitution")
181+
fi
182+
183+
# Skip if placeholder or empty
184+
if [ -n "$acronym" ] && [ "$acronym" != "[PROJECT_ACRONYM]" ]; then
185+
echo "$acronym"
186+
return
187+
fi
188+
189+
# Fallback: derive from H1 heading (e.g., "# Upwork Routine Automation Constitution")
190+
local heading
191+
heading=$(grep -m1 '^# ' "$constitution" | sed 's/^# //')
192+
if [ -z "$heading" ]; then
193+
echo ""
194+
return
195+
fi
196+
197+
# Skip if heading is still a placeholder
198+
if echo "$heading" | grep -q '\[PROJECT_NAME\]'; then
199+
echo ""
200+
return
201+
fi
202+
203+
# Remove trailing "Constitution" if present
204+
heading=$(echo "$heading" | sed 's/[[:space:]]*Constitution[[:space:]]*$//')
205+
206+
# Count words
207+
local word_count
208+
word_count=$(echo "$heading" | wc -w | tr -d ' ')
209+
210+
if [ "$word_count" -eq 1 ]; then
211+
# Single word: first 3 letters uppercased
212+
echo "$heading" | tr '[:lower:]' '[:upper:]' | cut -c1-3
213+
elif [ "$word_count" -ge 2 ]; then
214+
# Multiple words: first letter of each word
215+
echo "$heading" | tr '[:lower:]' '[:upper:]' | sed 's/[[:space:]]\+/ /g' | sed 's/\([A-Z]\)[^ ]*/\1/g' | tr -d ' '
216+
else
217+
echo ""
218+
fi
219+
}
220+
158221
# Resolve repository root. Prefer git information when available, but fall back
159222
# to searching for repository markers so the workflow still functions in repositories that
160223
# were initialised with --no-git.
@@ -248,24 +311,69 @@ fi
248311

249312
# Force base-10 interpretation to prevent octal conversion (e.g., 010 → 8 in octal, but should be 10 in decimal)
250313
FEATURE_NUM=$(printf "%03d" "$((10#$BRANCH_NUMBER))")
251-
BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}"
314+
315+
# Get project acronym from constitution
316+
PROJECT_ACRONYM=$(get_project_acronym "$REPO_ROOT")
317+
318+
# If no acronym found, ask the user
319+
if [ -z "$PROJECT_ACRONYM" ]; then
320+
CONSTITUTION_FILE="$REPO_ROOT/.specify/memory/constitution.md"
321+
>&2 echo ""
322+
>&2 printf "[specify] Enter PROJECT_ACRONYM (2-5 uppercase letters, or press Enter to skip): "
323+
read -r user_acronym || user_acronym=""
324+
# Uppercase and trim
325+
user_acronym=$(echo "$user_acronym" | tr '[:lower:]' '[:upper:]' | tr -d '[:space:]')
326+
if [[ "$user_acronym" =~ ^[A-Z]{2,5}$ ]]; then
327+
PROJECT_ACRONYM="$user_acronym"
328+
# Persist to constitution if file exists
329+
if [ -f "$CONSTITUTION_FILE" ]; then
330+
if head -1 "$CONSTITUTION_FILE" | grep -q '^---$'; then
331+
if grep -q '^project_acronym:' "$CONSTITUTION_FILE"; then
332+
sed -i.bak "s/^project_acronym:.*$/project_acronym: \"$PROJECT_ACRONYM\"/" "$CONSTITUTION_FILE"
333+
rm -f "$CONSTITUTION_FILE.bak"
334+
else
335+
sed -i.bak "1a\\
336+
project_acronym: \"$PROJECT_ACRONYM\"" "$CONSTITUTION_FILE"
337+
rm -f "$CONSTITUTION_FILE.bak"
338+
fi
339+
>&2 echo "[specify] Saved PROJECT_ACRONYM=$PROJECT_ACRONYM to constitution."
340+
fi
341+
fi
342+
elif [ -n "$user_acronym" ]; then
343+
>&2 echo "[specify] Invalid acronym (must be 2-5 uppercase letters). Skipping."
344+
fi
345+
fi
346+
347+
if [ -n "$PROJECT_ACRONYM" ]; then
348+
BRANCH_NAME="feature/${PROJECT_ACRONYM}-${FEATURE_NUM}-${BRANCH_SUFFIX}"
349+
else
350+
BRANCH_NAME="feature/${FEATURE_NUM}-${BRANCH_SUFFIX}"
351+
fi
252352

253353
# GitHub enforces a 244-byte limit on branch names
254354
# Validate and truncate if necessary
255355
MAX_BRANCH_LENGTH=244
256356
if [ ${#BRANCH_NAME} -gt $MAX_BRANCH_LENGTH ]; then
257-
# Calculate how much we need to trim from suffix
258-
# Account for: feature number (3) + hyphen (1) = 4 chars
259-
MAX_SUFFIX_LENGTH=$((MAX_BRANCH_LENGTH - 4))
260-
357+
# Calculate prefix length: "feature/" (8) + optional acronym + hyphen + feature number (3) + hyphen (1)
358+
if [ -n "$PROJECT_ACRONYM" ]; then
359+
PREFIX_LENGTH=$((8 + ${#PROJECT_ACRONYM} + 1 + 3 + 1))
360+
else
361+
PREFIX_LENGTH=$((8 + 3 + 1))
362+
fi
363+
MAX_SUFFIX_LENGTH=$((MAX_BRANCH_LENGTH - PREFIX_LENGTH))
364+
261365
# Truncate suffix at word boundary if possible
262366
TRUNCATED_SUFFIX=$(echo "$BRANCH_SUFFIX" | cut -c1-$MAX_SUFFIX_LENGTH)
263367
# Remove trailing hyphen if truncation created one
264368
TRUNCATED_SUFFIX=$(echo "$TRUNCATED_SUFFIX" | sed 's/-$//')
265-
369+
266370
ORIGINAL_BRANCH_NAME="$BRANCH_NAME"
267-
BRANCH_NAME="${FEATURE_NUM}-${TRUNCATED_SUFFIX}"
268-
371+
if [ -n "$PROJECT_ACRONYM" ]; then
372+
BRANCH_NAME="feature/${PROJECT_ACRONYM}-${FEATURE_NUM}-${TRUNCATED_SUFFIX}"
373+
else
374+
BRANCH_NAME="feature/${FEATURE_NUM}-${TRUNCATED_SUFFIX}"
375+
fi
376+
269377
>&2 echo "[specify] Warning: Branch name exceeded GitHub's 244-byte limit"
270378
>&2 echo "[specify] Original: $ORIGINAL_BRANCH_NAME (${#ORIGINAL_BRANCH_NAME} bytes)"
271379
>&2 echo "[specify] Truncated to: $BRANCH_NAME (${#BRANCH_NAME} bytes)"
@@ -277,7 +385,9 @@ else
277385
>&2 echo "[specify] Warning: Git repository not detected; skipped branch creation for $BRANCH_NAME"
278386
fi
279387

280-
FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME"
388+
# Strip feature/ prefix for spec directory name (avoids specs/feature/ nesting)
389+
SPEC_DIR_NAME="${BRANCH_NAME#feature/}"
390+
FEATURE_DIR="$SPECS_DIR/$SPEC_DIR_NAME"
281391
mkdir -p "$FEATURE_DIR"
282392

283393
TEMPLATE="$REPO_ROOT/.specify/templates/spec-template.md"
@@ -288,10 +398,11 @@ if [ -f "$TEMPLATE" ]; then cp "$TEMPLATE" "$SPEC_FILE"; else touch "$SPEC_FILE"
288398
export SPECIFY_FEATURE="$BRANCH_NAME"
289399

290400
if $JSON_MODE; then
291-
printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s"}\n' "$BRANCH_NAME" "$SPEC_FILE" "$FEATURE_NUM"
401+
printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s","PROJECT_ACRONYM":"%s"}\n' "$BRANCH_NAME" "$SPEC_FILE" "$FEATURE_NUM" "$PROJECT_ACRONYM"
292402
else
293403
echo "BRANCH_NAME: $BRANCH_NAME"
294404
echo "SPEC_FILE: $SPEC_FILE"
295405
echo "FEATURE_NUM: $FEATURE_NUM"
406+
echo "PROJECT_ACRONYM: $PROJECT_ACRONYM"
296407
echo "SPECIFY_FEATURE environment variable set to: $BRANCH_NAME"
297408
fi

scripts/powershell/common.ps1

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@ function Get-CurrentBranch {
4040
$highest = 0
4141

4242
Get-ChildItem -Path $specsDir -Directory | ForEach-Object {
43-
if ($_.Name -match '^(\d{3})-') {
43+
# Match both "001-name" and "ACR-001-name" directory patterns
44+
$dirName = $_.Name -replace '^[A-Z]{2,5}-', ''
45+
if ($dirName -match '^(\d{3})-') {
4446
$num = [int]$matches[1]
4547
if ($num -gt $highest) {
4648
$highest = $num
@@ -79,17 +81,18 @@ function Test-FeatureBranch {
7981
return $true
8082
}
8183

82-
if ($Branch -notmatch '^[0-9]{3}-') {
84+
if ($Branch -notmatch '^(feature/([A-Z]+-)?)?[0-9]{3}-') {
8385
Write-Output "ERROR: Not on a feature branch. Current branch: $Branch"
84-
Write-Output "Feature branches should be named like: 001-feature-name"
86+
Write-Output "Feature branches should be named like: feature/001-feature-name or feature/URA-001-feature-name"
8587
return $false
8688
}
8789
return $true
8890
}
8991

9092
function Get-FeatureDir {
9193
param([string]$RepoRoot, [string]$Branch)
92-
Join-Path $RepoRoot "specs/$Branch"
94+
$dir = $Branch -replace '^feature/', ''
95+
Join-Path $RepoRoot "specs/$dir"
9396
}
9497

9598
function Get-FeaturePathsEnv {

0 commit comments

Comments
 (0)