@@ -127,6 +127,16 @@ function Test-HasGit {
127127 }
128128}
129129
130+ # Strip a single optional path segment (e.g. gitflow "feat/004-name" -> "004-name").
131+ # Only when the full name is exactly two slash-free segments; otherwise returns the raw name.
132+ function Get-SpecKitEffectiveBranchName {
133+ param ([string ]$Branch )
134+ if ($Branch -match ' ^([^/]+)/([^/]+)$' ) {
135+ return $Matches [2 ]
136+ }
137+ return $Branch
138+ }
139+
130140function Test-FeatureBranch {
131141 param (
132142 [string ]$Branch ,
@@ -138,22 +148,69 @@ function Test-FeatureBranch {
138148 Write-Warning " [specify] Warning: Git repository not detected; skipped branch validation"
139149 return $true
140150 }
151+
152+ $raw = $Branch
153+ $Branch = Get-SpecKitEffectiveBranchName $raw
141154
142155 # Accept sequential prefix (3+ digits) but exclude malformed timestamps
143156 # Malformed: 7-or-8 digit date + 6-digit time with no trailing slug (e.g. "2026031-143022" or "20260319-143022")
144157 $hasMalformedTimestamp = ($Branch -match ' ^[0-9]{7}-[0-9]{6}-' ) -or ($Branch -match ' ^(?:\d{7}|\d{8})-\d{6}$' )
145158 $isSequential = ($Branch -match ' ^[0-9]{3,}-' ) -and (-not $hasMalformedTimestamp )
146159 if (-not $isSequential -and $Branch -notmatch ' ^\d{8}-\d{6}-' ) {
147- Write-Output " ERROR: Not on a feature branch. Current branch: $Branch "
148- Write-Output " Feature branches should be named like: 001-feature-name, 1234-feature-name, or 20260319-143022-feature-name"
160+ [ Console ]::Error.WriteLine( " ERROR: Not on a feature branch. Current branch: $raw " )
161+ [ Console ]::Error.WriteLine( " Feature branches should be named like: 001-feature-name, 1234-feature-name, or 20260319-143022-feature-name" )
149162 return $false
150163 }
151164 return $true
152165}
153166
154- function Get-FeatureDir {
155- param ([string ]$RepoRoot , [string ]$Branch )
156- Join-Path $RepoRoot " specs/$Branch "
167+ # Resolve specs/<feature-dir> by numeric/timestamp prefix (mirrors scripts/bash/common.sh find_feature_dir_by_prefix).
168+ function Find-FeatureDirByPrefix {
169+ param (
170+ [Parameter (Mandatory = $true )][string ]$RepoRoot ,
171+ [Parameter (Mandatory = $true )][string ]$Branch
172+ )
173+ $specsDir = Join-Path $RepoRoot ' specs'
174+ $branchName = Get-SpecKitEffectiveBranchName $Branch
175+
176+ $prefix = $null
177+ if ($branchName -match ' ^(\d{8}-\d{6})-' ) {
178+ $prefix = $Matches [1 ]
179+ } elseif ($branchName -match ' ^(\d{3,})-' ) {
180+ $prefix = $Matches [1 ]
181+ } else {
182+ return (Join-Path $specsDir $branchName )
183+ }
184+
185+ $dirMatches = @ ()
186+ if (Test-Path - LiteralPath $specsDir - PathType Container) {
187+ $dirMatches = @ (Get-ChildItem - LiteralPath $specsDir - Filter " $prefix -*" - Directory - ErrorAction SilentlyContinue)
188+ }
189+
190+ if ($dirMatches.Count -eq 0 ) {
191+ return (Join-Path $specsDir $branchName )
192+ }
193+ if ($dirMatches.Count -eq 1 ) {
194+ return $dirMatches [0 ].FullName
195+ }
196+ $names = ($dirMatches | ForEach-Object { $_.Name }) -join ' '
197+ [Console ]::Error.WriteLine(" ERROR: Multiple spec directories found with prefix '$prefix ': $names " )
198+ [Console ]::Error.WriteLine(' Please ensure only one spec directory exists per prefix.' )
199+ return $null
200+ }
201+
202+ # Branch-based prefix resolution; mirrors bash get_feature_paths failure (stderr + exit 1).
203+ function Get-FeatureDirFromBranchPrefixOrExit {
204+ param (
205+ [Parameter (Mandatory = $true )][string ]$RepoRoot ,
206+ [Parameter (Mandatory = $true )][string ]$CurrentBranch
207+ )
208+ $resolved = Find-FeatureDirByPrefix - RepoRoot $RepoRoot - Branch $CurrentBranch
209+ if ($null -eq $resolved ) {
210+ [Console ]::Error.WriteLine(' ERROR: Failed to resolve feature directory' )
211+ exit 1
212+ }
213+ return $resolved
157214}
158215
159216function Get-FeaturePathsEnv {
@@ -164,7 +221,7 @@ function Get-FeaturePathsEnv {
164221 # Resolve feature directory. Priority:
165222 # 1. SPECIFY_FEATURE_DIRECTORY env var (explicit override)
166223 # 2. .specify/feature.json "feature_directory" key (persisted by /speckit.specify)
167- # 3. Exact branch-to-directory mapping via Get-FeatureDir (legacy fallback )
224+ # 3. Branch-name-based prefix lookup (same as scripts/bash/common.sh )
168225 $featureJson = Join-Path $repoRoot ' .specify/feature.json'
169226 if ($env: SPECIFY_FEATURE_DIRECTORY ) {
170227 $featureDir = $env: SPECIFY_FEATURE_DIRECTORY
@@ -173,22 +230,24 @@ function Get-FeaturePathsEnv {
173230 $featureDir = Join-Path $repoRoot $featureDir
174231 }
175232 } elseif (Test-Path $featureJson ) {
233+ $featureJsonRaw = Get-Content - LiteralPath $featureJson - Raw
176234 try {
177- $featureConfig = Get-Content $featureJson - Raw | ConvertFrom-Json
178- if ($featureConfig.feature_directory ) {
179- $featureDir = $featureConfig.feature_directory
180- # Normalize relative paths to absolute under repo root
181- if (-not [System.IO.Path ]::IsPathRooted($featureDir )) {
182- $featureDir = Join-Path $repoRoot $featureDir
183- }
184- } else {
185- $featureDir = Get-FeatureDir - RepoRoot $repoRoot - Branch $currentBranch
186- }
235+ $featureConfig = $featureJsonRaw | ConvertFrom-Json
187236 } catch {
188- $featureDir = Get-FeatureDir - RepoRoot $repoRoot - Branch $currentBranch
237+ [Console ]::Error.WriteLine(" ERROR: Failed to parse .specify/feature.json: $_ " )
238+ exit 1
239+ }
240+ if ($featureConfig.feature_directory ) {
241+ $featureDir = $featureConfig.feature_directory
242+ # Normalize relative paths to absolute under repo root
243+ if (-not [System.IO.Path ]::IsPathRooted($featureDir )) {
244+ $featureDir = Join-Path $repoRoot $featureDir
245+ }
246+ } else {
247+ $featureDir = Get-FeatureDirFromBranchPrefixOrExit - RepoRoot $repoRoot - CurrentBranch $currentBranch
189248 }
190249 } else {
191- $featureDir = Get-FeatureDir - RepoRoot $repoRoot - Branch $currentBranch
250+ $featureDir = Get-FeatureDirFromBranchPrefixOrExit - RepoRoot $repoRoot - CurrentBranch $currentBranch
192251 }
193252
194253 [PSCustomObject ]@ {
0 commit comments