Refactor feature script logic and update agent context scripts

Refactored both Bash and PowerShell create-new-feature scripts to modularize and deduplicate logic for determining the next feature number, including new helper functions for extracting the highest number from specs and branches. Improved branch name cleaning and generation. In update-agent-context scripts, removed redundant updates to AGENTS.md for Copilot, streamlining agent update logic.
This commit is contained in:
den (work)
2025-11-07 13:17:35 -08:00
parent 24b6d31471
commit 3dcbb6e3a9
4 changed files with 110 additions and 124 deletions

View File

@@ -80,9 +80,56 @@ find_repo_root() {
return 1 return 1
} }
# Function to get highest number from specs directory
get_highest_from_specs() {
local specs_dir="$1"
local highest=0
if [ -d "$specs_dir" ]; then
for dir in "$specs_dir"/*; do
[ -d "$dir" ] || continue
dirname=$(basename "$dir")
number=$(echo "$dirname" | grep -o '^[0-9]\+' || echo "0")
number=$((10#$number))
if [ "$number" -gt "$highest" ]; then
highest=$number
fi
done
fi
echo "$highest"
}
# Function to get highest number from git branches
get_highest_from_branches() {
local highest=0
# Get all branches (local and remote)
branches=$(git branch -a 2>/dev/null || echo "")
if [ -n "$branches" ]; then
while IFS= read -r branch; do
# 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
number=$(echo "$clean_branch" | grep -o '^[0-9]\{3\}' || echo "0")
number=$((10#$number))
if [ "$number" -gt "$highest" ]; then
highest=$number
fi
fi
done <<< "$branches"
fi
echo "$highest"
}
# Function to check existing branches (local and remote) and return next available number # Function to check existing branches (local and remote) and return next available number
check_existing_branches() { check_existing_branches() {
local short_name="$1" local short_name="$1"
local specs_dir="$2"
# Fetch all remotes to get latest branch info (suppress errors if no remotes) # Fetch all remotes to get latest branch info (suppress errors if no remotes)
git fetch --all --prune 2>/dev/null || true git fetch --all --prune 2>/dev/null || true
@@ -95,8 +142,8 @@ check_existing_branches() {
# Check specs directory as well # Check specs directory as well
local spec_dirs="" local spec_dirs=""
if [ -d "$SPECS_DIR" ]; then if [ -d "$specs_dir" ]; then
spec_dirs=$(find "$SPECS_DIR" -maxdepth 1 -type d -name "[0-9]*-${short_name}" 2>/dev/null | xargs -n1 basename 2>/dev/null | sed 's/-.*//' | sort -n) spec_dirs=$(find "$specs_dir" -maxdepth 1 -type d -name "[0-9]*-${short_name}" 2>/dev/null | xargs -n1 basename 2>/dev/null | sed 's/-.*//' | sort -n)
fi fi
# Combine all sources and get the highest number # Combine all sources and get the highest number
@@ -111,6 +158,12 @@ check_existing_branches() {
echo $((max_num + 1)) echo $((max_num + 1))
} }
# Function to clean and format a branch name
clean_branch_name() {
local name="$1"
echo "$name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//'
}
# Resolve repository root. Prefer git information when available, but fall back # Resolve repository root. Prefer git information when available, but fall back
# to searching for repository markers so the workflow still functions in repositories that # to searching for repository markers so the workflow still functions in repositories that
# were initialised with --no-git. # were initialised with --no-git.
@@ -133,48 +186,6 @@ cd "$REPO_ROOT"
SPECS_DIR="$REPO_ROOT/specs" SPECS_DIR="$REPO_ROOT/specs"
mkdir -p "$SPECS_DIR" mkdir -p "$SPECS_DIR"
# Get highest number from specs directory
HIGHEST_FROM_SPECS=0
if [ -d "$SPECS_DIR" ]; then
for dir in "$SPECS_DIR"/*; do
[ -d "$dir" ] || continue
dirname=$(basename "$dir")
number=$(echo "$dirname" | grep -o '^[0-9]\+' || echo "0")
number=$((10#$number))
if [ "$number" -gt "$HIGHEST_FROM_SPECS" ]; then HIGHEST_FROM_SPECS=$number; fi
done
fi
# Get highest number from branch names (both local and remote)
HIGHEST_FROM_BRANCHES=0
if [ "$HAS_GIT" = true ]; then
# Get all branches (local and remote)
branches=$(git branch -a 2>/dev/null || echo "")
if [ -n "$branches" ]; then
while IFS= read -r branch; do
# 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
number=$(echo "$clean_branch" | grep -o '^[0-9]\{3\}' || echo "0")
number=$((10#$number))
if [ "$number" -gt "$HIGHEST_FROM_BRANCHES" ]; then HIGHEST_FROM_BRANCHES=$number; fi
fi
done <<< "$branches"
fi
fi
# Use the highest number from either source
HIGHEST=$HIGHEST_FROM_SPECS
if [ "$HIGHEST_FROM_BRANCHES" -gt "$HIGHEST" ]; then
HIGHEST=$HIGHEST_FROM_BRANCHES
fi
NEXT=$((HIGHEST + 1))
FEATURE_NUM=$(printf "%03d" "$NEXT")
# Function to generate branch name with stop word filtering and length filtering # Function to generate branch name with stop word filtering and length filtering
generate_branch_name() { generate_branch_name() {
local description="$1" local description="$1"
@@ -218,14 +229,15 @@ generate_branch_name() {
echo "$result" echo "$result"
else else
# Fallback to original logic if no meaningful words found # Fallback to original logic if no meaningful words found
echo "$description" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//' | tr '-' '\n' | grep -v '^$' | head -3 | tr '\n' '-' | sed 's/-$//' local cleaned=$(clean_branch_name "$description")
echo "$cleaned" | tr '-' '\n' | grep -v '^$' | head -3 | tr '\n' '-' | sed 's/-$//'
fi fi
} }
# Generate branch name # Generate branch name
if [ -n "$SHORT_NAME" ]; then if [ -n "$SHORT_NAME" ]; then
# Use provided short name, just clean it up # Use provided short name, just clean it up
BRANCH_SUFFIX=$(echo "$SHORT_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//') BRANCH_SUFFIX=$(clean_branch_name "$SHORT_NAME")
else else
# Generate from description with smart filtering # Generate from description with smart filtering
BRANCH_SUFFIX=$(generate_branch_name "$FEATURE_DESCRIPTION") BRANCH_SUFFIX=$(generate_branch_name "$FEATURE_DESCRIPTION")
@@ -235,19 +247,10 @@ fi
if [ -z "$BRANCH_NUMBER" ]; then if [ -z "$BRANCH_NUMBER" ]; then
if [ "$HAS_GIT" = true ]; then if [ "$HAS_GIT" = true ]; then
# Check existing branches on remotes # Check existing branches on remotes
BRANCH_NUMBER=$(check_existing_branches "$BRANCH_SUFFIX") BRANCH_NUMBER=$(check_existing_branches "$BRANCH_SUFFIX" "$SPECS_DIR")
else else
# Fall back to local directory check # Fall back to local directory check
HIGHEST=0 HIGHEST=$(get_highest_from_specs "$SPECS_DIR")
if [ -d "$SPECS_DIR" ]; then
for dir in "$SPECS_DIR"/*; do
[ -d "$dir" ] || continue
dirname=$(basename "$dir")
number=$(echo "$dirname" | grep -o '^[0-9]\+' || echo "0")
number=$((10#$number))
if [ "$number" -gt "$HIGHEST" ]; then HIGHEST=$number; fi
done
fi
BRANCH_NUMBER=$((HIGHEST + 1)) BRANCH_NUMBER=$((HIGHEST + 1))
fi fi
fi fi

View File

@@ -62,7 +62,6 @@ AGENT_TYPE="${1:-}"
CLAUDE_FILE="$REPO_ROOT/CLAUDE.md" CLAUDE_FILE="$REPO_ROOT/CLAUDE.md"
GEMINI_FILE="$REPO_ROOT/GEMINI.md" GEMINI_FILE="$REPO_ROOT/GEMINI.md"
COPILOT_FILE="$REPO_ROOT/.github/agents/copilot-instructions.md" COPILOT_FILE="$REPO_ROOT/.github/agents/copilot-instructions.md"
COPILOT_AGENTS_FILE="$REPO_ROOT/AGENTS.md"
CURSOR_FILE="$REPO_ROOT/.cursor/rules/specify-rules.mdc" CURSOR_FILE="$REPO_ROOT/.cursor/rules/specify-rules.mdc"
QWEN_FILE="$REPO_ROOT/QWEN.md" QWEN_FILE="$REPO_ROOT/QWEN.md"
AGENTS_FILE="$REPO_ROOT/AGENTS.md" AGENTS_FILE="$REPO_ROOT/AGENTS.md"
@@ -588,7 +587,6 @@ update_specific_agent() {
;; ;;
copilot) copilot)
update_agent_file "$COPILOT_FILE" "GitHub Copilot" update_agent_file "$COPILOT_FILE" "GitHub Copilot"
update_agent_file "$COPILOT_AGENTS_FILE" "GitHub Copilot (AGENTS.md)"
;; ;;
cursor-agent) cursor-agent)
update_agent_file "$CURSOR_FILE" "Cursor IDE" update_agent_file "$CURSOR_FILE" "Cursor IDE"
@@ -650,12 +648,6 @@ update_all_existing_agents() {
found_agent=true found_agent=true
fi fi
# Also update AGENTS.md for Copilot if the Copilot directory exists
if [[ -d "$REPO_ROOT/.github/agents" ]]; then
update_agent_file "$COPILOT_AGENTS_FILE" "GitHub Copilot (AGENTS.md)"
found_agent=true
fi
if [[ -f "$CURSOR_FILE" ]]; then if [[ -f "$CURSOR_FILE" ]]; then
update_agent_file "$CURSOR_FILE" "Cursor IDE" update_agent_file "$CURSOR_FILE" "Cursor IDE"
found_agent=true found_agent=true

View File

@@ -59,6 +59,46 @@ function Find-RepositoryRoot {
} }
} }
function Get-HighestNumberFromSpecs {
param([string]$SpecsDir)
$highest = 0
if (Test-Path $SpecsDir) {
Get-ChildItem -Path $SpecsDir -Directory | ForEach-Object {
if ($_.Name -match '^(\d+)') {
$num = [int]$matches[1]
if ($num -gt $highest) { $highest = $num }
}
}
}
return $highest
}
function Get-HighestNumberFromBranches {
param()
$highest = 0
try {
$branches = git branch -a 2>$null
if ($LASTEXITCODE -eq 0) {
foreach ($branch in $branches) {
# Clean branch name: remove leading markers and remote prefixes
$cleanBranch = $branch.Trim() -replace '^\*?\s+', '' -replace '^remotes/[^/]+/', ''
# Extract feature number if branch matches pattern ###-*
if ($cleanBranch -match '^(\d+)-') {
$num = [int]$matches[1]
if ($num -gt $highest) { $highest = $num }
}
}
}
} catch {
# If git command fails, return 0
Write-Verbose "Could not check Git branches: $_"
}
return $highest
}
function Get-NextBranchNumber { function Get-NextBranchNumber {
param( param(
[string]$ShortName, [string]$ShortName,
@@ -127,6 +167,12 @@ function Get-NextBranchNumber {
# Return next number # Return next number
return $maxNum + 1 return $maxNum + 1
} }
function ConvertTo-CleanBranchName {
param([string]$Name)
return $Name.ToLower() -replace '[^a-z0-9]', '-' -replace '-{2,}', '-' -replace '^-', '' -replace '-$', ''
}
$fallbackRoot = (Find-RepositoryRoot -StartDir $PSScriptRoot) $fallbackRoot = (Find-RepositoryRoot -StartDir $PSScriptRoot)
if (-not $fallbackRoot) { if (-not $fallbackRoot) {
Write-Error "Error: Could not determine repository root. Please run this script from within the repository." Write-Error "Error: Could not determine repository root. Please run this script from within the repository."
@@ -150,46 +196,6 @@ Set-Location $repoRoot
$specsDir = Join-Path $repoRoot 'specs' $specsDir = Join-Path $repoRoot 'specs'
New-Item -ItemType Directory -Path $specsDir -Force | Out-Null New-Item -ItemType Directory -Path $specsDir -Force | Out-Null
# Get highest number from specs directory
$highestFromSpecs = 0
if (Test-Path $specsDir) {
Get-ChildItem -Path $specsDir -Directory | ForEach-Object {
if ($_.Name -match '^(\d{3})') {
$num = [int]$matches[1]
if ($num -gt $highestFromSpecs) { $highestFromSpecs = $num }
}
}
}
# Get highest number from branch names (both local and remote)
$highestFromBranches = 0
try {
$branches = git branch -a 2>$null
if ($LASTEXITCODE -eq 0) {
foreach ($branch in $branches) {
# Clean branch name: remove leading markers and remote prefixes
# The following regex removes:
# - Git's current branch marker ('*') and leading whitespace (e.g., '* main')
# - Remote prefixes (e.g., 'remotes/origin/')
$cleanBranch = $branch.Trim() -replace '^\*?\s+', '' -replace '^remotes/[^/]+/', ''
# Extract feature number if branch matches pattern ###-*
if ($cleanBranch -match '^(\d{3})-') {
$num = [int]$matches[1]
if ($num -gt $highestFromBranches) { $highestFromBranches = $num }
}
}
}
} catch {
# If git command fails, just continue with specs-only check
Write-Verbose "Could not check Git branches: $_"
}
# Use the highest number from either source
$highest = [Math]::Max($highestFromSpecs, $highestFromBranches)
$next = $highest + 1
$featureNum = ('{0:000}' -f $next)
# Function to generate branch name with stop word filtering and length filtering # Function to generate branch name with stop word filtering and length filtering
function Get-BranchName { function Get-BranchName {
param([string]$Description) param([string]$Description)
@@ -229,7 +235,7 @@ function Get-BranchName {
return $result return $result
} else { } else {
# Fallback to original logic if no meaningful words found # Fallback to original logic if no meaningful words found
$result = $Description.ToLower() -replace '[^a-z0-9]', '-' -replace '-{2,}', '-' -replace '^-', '' -replace '-$', '' $result = ConvertTo-CleanBranchName -Name $Description
$fallbackWords = ($result -split '-') | Where-Object { $_ } | Select-Object -First 3 $fallbackWords = ($result -split '-') | Where-Object { $_ } | Select-Object -First 3
return [string]::Join('-', $fallbackWords) return [string]::Join('-', $fallbackWords)
} }
@@ -238,7 +244,7 @@ function Get-BranchName {
# Generate branch name # Generate branch name
if ($ShortName) { if ($ShortName) {
# Use provided short name, just clean it up # Use provided short name, just clean it up
$branchSuffix = $ShortName.ToLower() -replace '[^a-z0-9]', '-' -replace '-{2,}', '-' -replace '^-', '' -replace '-$', '' $branchSuffix = ConvertTo-CleanBranchName -Name $ShortName
} else { } else {
# Generate from description with smart filtering # Generate from description with smart filtering
$branchSuffix = Get-BranchName -Description $featureDesc $branchSuffix = Get-BranchName -Description $featureDesc
@@ -251,16 +257,7 @@ if ($Number -eq 0) {
$Number = Get-NextBranchNumber -ShortName $branchSuffix -SpecsDir $specsDir $Number = Get-NextBranchNumber -ShortName $branchSuffix -SpecsDir $specsDir
} else { } else {
# Fall back to local directory check # Fall back to local directory check
$highest = 0 $Number = (Get-HighestNumberFromSpecs -SpecsDir $specsDir) + 1
if (Test-Path $specsDir) {
Get-ChildItem -Path $specsDir -Directory | ForEach-Object {
if ($_.Name -match '^(\d{3})') {
$num = [int]$matches[1]
if ($num -gt $highest) { $highest = $num }
}
}
}
$Number = $highest + 1
} }
} }

View File

@@ -47,7 +47,6 @@ $NEW_PLAN = $IMPL_PLAN
$CLAUDE_FILE = Join-Path $REPO_ROOT 'CLAUDE.md' $CLAUDE_FILE = Join-Path $REPO_ROOT 'CLAUDE.md'
$GEMINI_FILE = Join-Path $REPO_ROOT 'GEMINI.md' $GEMINI_FILE = Join-Path $REPO_ROOT 'GEMINI.md'
$COPILOT_FILE = Join-Path $REPO_ROOT '.github/agents/copilot-instructions.md' $COPILOT_FILE = Join-Path $REPO_ROOT '.github/agents/copilot-instructions.md'
$COPILOT_AGENTS_FILE = Join-Path $REPO_ROOT 'AGENTS.md'
$CURSOR_FILE = Join-Path $REPO_ROOT '.cursor/rules/specify-rules.mdc' $CURSOR_FILE = Join-Path $REPO_ROOT '.cursor/rules/specify-rules.mdc'
$QWEN_FILE = Join-Path $REPO_ROOT 'QWEN.md' $QWEN_FILE = Join-Path $REPO_ROOT 'QWEN.md'
$AGENTS_FILE = Join-Path $REPO_ROOT 'AGENTS.md' $AGENTS_FILE = Join-Path $REPO_ROOT 'AGENTS.md'
@@ -371,10 +370,7 @@ function Update-SpecificAgent {
switch ($Type) { switch ($Type) {
'claude' { Update-AgentFile -TargetFile $CLAUDE_FILE -AgentName 'Claude Code' } 'claude' { Update-AgentFile -TargetFile $CLAUDE_FILE -AgentName 'Claude Code' }
'gemini' { Update-AgentFile -TargetFile $GEMINI_FILE -AgentName 'Gemini CLI' } 'gemini' { Update-AgentFile -TargetFile $GEMINI_FILE -AgentName 'Gemini CLI' }
'copilot' { 'copilot' { Update-AgentFile -TargetFile $COPILOT_FILE -AgentName 'GitHub Copilot' }
Update-AgentFile -TargetFile $COPILOT_FILE -AgentName 'GitHub Copilot'
Update-AgentFile -TargetFile $COPILOT_AGENTS_FILE -AgentName 'GitHub Copilot (AGENTS.md)'
}
'cursor-agent' { Update-AgentFile -TargetFile $CURSOR_FILE -AgentName 'Cursor IDE' } 'cursor-agent' { Update-AgentFile -TargetFile $CURSOR_FILE -AgentName 'Cursor IDE' }
'qwen' { Update-AgentFile -TargetFile $QWEN_FILE -AgentName 'Qwen Code' } 'qwen' { Update-AgentFile -TargetFile $QWEN_FILE -AgentName 'Qwen Code' }
'opencode' { Update-AgentFile -TargetFile $AGENTS_FILE -AgentName 'opencode' } 'opencode' { Update-AgentFile -TargetFile $AGENTS_FILE -AgentName 'opencode' }
@@ -396,8 +392,6 @@ function Update-AllExistingAgents {
if (Test-Path $CLAUDE_FILE) { if (-not (Update-AgentFile -TargetFile $CLAUDE_FILE -AgentName 'Claude Code')) { $ok = $false }; $found = $true } if (Test-Path $CLAUDE_FILE) { if (-not (Update-AgentFile -TargetFile $CLAUDE_FILE -AgentName 'Claude Code')) { $ok = $false }; $found = $true }
if (Test-Path $GEMINI_FILE) { if (-not (Update-AgentFile -TargetFile $GEMINI_FILE -AgentName 'Gemini CLI')) { $ok = $false }; $found = $true } if (Test-Path $GEMINI_FILE) { if (-not (Update-AgentFile -TargetFile $GEMINI_FILE -AgentName 'Gemini CLI')) { $ok = $false }; $found = $true }
if (Test-Path $COPILOT_FILE) { if (-not (Update-AgentFile -TargetFile $COPILOT_FILE -AgentName 'GitHub Copilot')) { $ok = $false }; $found = $true } if (Test-Path $COPILOT_FILE) { if (-not (Update-AgentFile -TargetFile $COPILOT_FILE -AgentName 'GitHub Copilot')) { $ok = $false }; $found = $true }
# Also update AGENTS.md for Copilot if the Copilot directory exists
if (Test-Path (Join-Path $REPO_ROOT '.github/agents')) { if (-not (Update-AgentFile -TargetFile $COPILOT_AGENTS_FILE -AgentName 'GitHub Copilot (AGENTS.md)')) { $ok = $false }; $found = $true }
if (Test-Path $CURSOR_FILE) { if (-not (Update-AgentFile -TargetFile $CURSOR_FILE -AgentName 'Cursor IDE')) { $ok = $false }; $found = $true } if (Test-Path $CURSOR_FILE) { if (-not (Update-AgentFile -TargetFile $CURSOR_FILE -AgentName 'Cursor IDE')) { $ok = $false }; $found = $true }
if (Test-Path $QWEN_FILE) { if (-not (Update-AgentFile -TargetFile $QWEN_FILE -AgentName 'Qwen Code')) { $ok = $false }; $found = $true } if (Test-Path $QWEN_FILE) { if (-not (Update-AgentFile -TargetFile $QWEN_FILE -AgentName 'Qwen Code')) { $ok = $false }; $found = $true }
if (Test-Path $AGENTS_FILE) { if (-not (Update-AgentFile -TargetFile $AGENTS_FILE -AgentName 'Codex/opencode')) { $ok = $false }; $found = $true } if (Test-Path $AGENTS_FILE) { if (-not (Update-AgentFile -TargetFile $AGENTS_FILE -AgentName 'Codex/opencode')) { $ok = $false }; $found = $true }