private/Resolve-WtwTarget.ps1

function Resolve-WtwTarget {
    <#
    .SYNOPSIS
        Resolves a name/alias to a repo + optional worktree entry.
    .DESCRIPTION
        Unified resolution logic used by Enter, Remove, Open, etc.
        Resolution order:
          1. Exact repo alias match -> returns repo (no worktree)
          1b. Repo name/alias prefix match -> unique prefix on repo names and aliases
          2. "alias-task" exact match -> returns repo + worktree
          3. Bare task name exact match -> searches all repos for unique match
          4. "alias-task" prefix match -> unique prefix on task name (proj-b -> proj-backend-refactor)
          5. Bare task name prefix match -> unique prefix across all repos
          5b. Substring match on task -> "content" matches "my-content-engine"
          6. Fuzzy match (Levenshtein) -> auto-resolve if unique close match, suggest if tied
    .OUTPUTS
        PSCustomObject with: RepoName, RepoEntry, TaskName, WorktreeEntry
        or $null if nothing matched.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, Position = 0)]
        [string] $Name
    )

    $registry = Get-WtwRegistry

    # 1. Exact repo alias -> main repo
    foreach ($repoName in $registry.repos.PSObject.Properties.Name) {
        $repo = $registry.repos.$repoName
        if ((Test-WtwAliasMatch $repo $Name) -or $repoName -eq $Name) {
            return [PSCustomObject]@{
                RepoName       = $repoName
                RepoEntry      = $repo
                TaskName       = $null
                WorktreeEntry  = $null
            }
        }
    }

    # 1b. Repo name/alias prefix match
    $prefixRepos = @()
    foreach ($repoName in $registry.repos.PSObject.Properties.Name) {
        $repo = $registry.repos.$repoName
        $matched = $false
        if ($repoName -like "${Name}*") { $matched = $true }
        if (-not $matched) {
            foreach ($alias in (Get-WtwRepoAliases $repo)) {
                if ($alias -like "${Name}*") { $matched = $true; break }
            }
        }
        if ($matched) {
            $prefixRepos += [PSCustomObject]@{
                RepoName       = $repoName
                RepoEntry      = $repo
                TaskName       = $null
                WorktreeEntry  = $null
            }
        }
    }
    if ($prefixRepos.Count -eq 1) { return $prefixRepos[0] }
    if ($prefixRepos.Count -gt 1) {
        $names = ($prefixRepos | ForEach-Object { $_.RepoName }) -join ', '
        Write-Error "Ambiguous prefix '$Name'. Matches repos: $names"
        return $null
    }

    # 2. "alias-task" exact match
    if ($Name -match '^(.+?)-(.+)$') {
        $aliasOrName = $Matches[1]
        $taskName    = $Matches[2]
        foreach ($repoName in $registry.repos.PSObject.Properties.Name) {
            $repo = $registry.repos.$repoName
            if (((Test-WtwAliasMatch $repo $aliasOrName) -or $repoName -eq $aliasOrName) -and
                $repo.worktrees -and $repo.worktrees.PSObject.Properties.Name -contains $taskName) {
                return [PSCustomObject]@{
                    RepoName       = $repoName
                    RepoEntry      = $repo
                    TaskName       = $taskName
                    WorktreeEntry  = $repo.worktrees.$taskName
                }
            }
        }
    }

    # 3. Bare task name exact match -> search all repos
    $found = @()
    foreach ($repoName in $registry.repos.PSObject.Properties.Name) {
        $repo = $registry.repos.$repoName
        if ($repo.worktrees -and $repo.worktrees.PSObject.Properties.Name -contains $Name) {
            $found += [PSCustomObject]@{
                RepoName       = $repoName
                RepoEntry      = $repo
                TaskName       = $Name
                WorktreeEntry  = $repo.worktrees.$Name
            }
        }
    }

    if ($found.Count -eq 1) { return $found[0] }
    if ($found.Count -gt 1) {
        Write-Error "Ambiguous name '$Name'. Found in multiple repos. Use 'alias-task' format."
        return $null
    }

    # 4. "alias-task" prefix match - proj-b matches proj-backend-refactor
    if ($Name -match '^(.+?)-(.+)$') {
        $aliasOrName  = $Matches[1]
        $taskPrefix   = $Matches[2]
        $prefixFound  = @()
        foreach ($repoName in $registry.repos.PSObject.Properties.Name) {
            $repo = $registry.repos.$repoName
            if (-not ((Test-WtwAliasMatch $repo $aliasOrName) -or $repoName -eq $aliasOrName)) { continue }
            if (-not $repo.worktrees) { continue }
            foreach ($t in $repo.worktrees.PSObject.Properties.Name) {
                if ($t -like "${taskPrefix}*") {
                    $prefixFound += [PSCustomObject]@{
                        RepoName       = $repoName
                        RepoEntry      = $repo
                        TaskName       = $t
                        WorktreeEntry  = $repo.worktrees.$t
                    }
                }
            }
        }
        if ($prefixFound.Count -eq 1) { return $prefixFound[0] }
        if ($prefixFound.Count -gt 1) {
            $names = ($prefixFound | ForEach-Object { $_.TaskName }) -join ', '
            Write-Error "Ambiguous prefix '$Name'. Matches: $names"
            return $null
        }
    }

    # 5. Bare task name prefix match -> search all repos
    $prefixFound = @()
    foreach ($repoName in $registry.repos.PSObject.Properties.Name) {
        $repo = $registry.repos.$repoName
        if (-not $repo.worktrees) { continue }
        foreach ($t in $repo.worktrees.PSObject.Properties.Name) {
            if ($t -like "${Name}*") {
                $prefixFound += [PSCustomObject]@{
                    RepoName       = $repoName
                    RepoEntry      = $repo
                    TaskName       = $t
                    WorktreeEntry  = $repo.worktrees.$t
                }
            }
        }
    }

    if ($prefixFound.Count -eq 1) { return $prefixFound[0] }
    if ($prefixFound.Count -gt 1) {
        $names = ($prefixFound | ForEach-Object { "$($_.RepoName)/$($_.TaskName)" }) -join ', '
        Write-Error "Ambiguous prefix '$Name'. Matches: $names"
        return $null
    }

    # 5b. Substring match on task names -> "content" matches "my-content-engine"
    $escapedName = [WildcardPattern]::Escape($Name)
    $substringFound = @()
    foreach ($repoName in $registry.repos.PSObject.Properties.Name) {
        $repo = $registry.repos.$repoName
        if (-not $repo.worktrees) { continue }
        foreach ($t in $repo.worktrees.PSObject.Properties.Name) {
            if ($t -like "*${escapedName}*") {
                $substringFound += [PSCustomObject]@{
                    RepoName       = $repoName
                    RepoEntry      = $repo
                    TaskName       = $t
                    WorktreeEntry  = $repo.worktrees.$t
                }
            }
        }
    }

    if ($substringFound.Count -eq 1) {
        Write-Verbose "Substring match: '$Name' -> '$($substringFound[0].TaskName)'"
        return $substringFound[0]
    }
    if ($substringFound.Count -gt 1) {
        $names = ($substringFound | ForEach-Object { "$($_.RepoName)/$($_.TaskName)" }) -join ', '
        Write-Error "Ambiguous substring '$Name'. Matches: $names"
        return $null
    }

    # 6. Fuzzy match - find closest target by edit distance
    $allTargets = Get-WtwAllTargetNames $registry
    $fuzzy = Resolve-WtwFuzzyMatch $Name $allTargets
    if ($fuzzy.Match) {
        return (Resolve-WtwTarget $fuzzy.Match)
    }
    if ($fuzzy.Suggestions.Count -gt 0) {
        $suggestions = $fuzzy.Suggestions -join ', '
        Write-Error "Could not resolve '$Name'. Did you mean: ${suggestions}?"
        return $null
    }

    Write-Error "Could not resolve '$Name'. Run 'wtw list' to see available targets."
    return $null
}