extensions/specrew-speckit/scripts/manage-reviewer-regression.ps1

[CmdletBinding()]
param(
    [Parameter(Mandatory = $true)]
    [ValidateSet('report', 'resolve', 'withdraw', 'project', 'get')]
    [string]$Mode,

    [Parameter(Mandatory = $false)]
    [string]$ProjectRoot = '.',

    [Parameter(Mandatory = $false)]
    [string]$EventId,

    [Parameter(Mandatory = $false)]
    [string]$Feature,

    [Parameter(Mandatory = $false)]
    [string]$IterationDirectory,

    [Parameter(Mandatory = $false)]
    [string]$Slice,

    [Parameter(Mandatory = $false)]
    [string]$PriorReviewerVerdict,

    [Parameter(Mandatory = $false)]
    [string]$PriorReviewerClass,

    [Parameter(Mandatory = $false)]
    [string]$PriorReviewerOwner,

    [Parameter(Mandatory = $false)]
    [string]$DefectDescription,

    [Parameter(Mandatory = $false)]
    [string]$DefectSourceLocation,

    [Parameter(Mandatory = $false)]
    [string]$ImplementerOwner
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

$scriptDir = Split-Path -Parent $PSCommandPath
. (Join-Path $scriptDir 'shared-governance.ps1')

$ProjectRoot = Resolve-ProjectPath -Path $ProjectRoot
if (-not [string]::IsNullOrWhiteSpace($IterationDirectory)) {
    $IterationDirectory = Resolve-ProjectPath -Path $IterationDirectory
}

function ConvertTo-BoolLike {
    param([AllowNull()]$Value)

    if ($null -eq $Value) {
        return $false
    }

    $text = ([string]$Value).Trim().Trim('"').Trim("'").ToLowerInvariant()
    return $text -in @('1', 'true', 'yes', 'on')
}

function Normalize-YamlValue {
    param([AllowNull()][string]$Value)

    if ($null -eq $Value) {
        return $null
    }

    $text = $Value.Trim()
    if ($text.Contains('#')) {
        $text = ($text -split '\s+#', 2)[0].Trim()
    }

    if (($text.StartsWith('"') -and $text.EndsWith('"')) -or ($text.StartsWith("'") -and $text.EndsWith("'"))) {
        $text = $text.Substring(1, $text.Length - 2)
    }

    if ($text -eq 'null') {
        return $null
    }

    return $text
}

function Get-IterationConfigPath {
    param([Parameter(Mandatory = $true)][string]$ProjectRoot)

    return Join-Path $ProjectRoot '.specrew\iteration-config.yml'
}

function Get-RoleAssignmentsPath {
    param([Parameter(Mandatory = $true)][string]$ProjectRoot)

    return Join-Path $ProjectRoot '.specrew\role-assignments.yml'
}

function Get-ReviewerRegressionSettings {
    param([Parameter(Mandatory = $true)][string]$ProjectRoot)

    $configPath = Get-IterationConfigPath -ProjectRoot $ProjectRoot
    $settings = [ordered]@{
        Enabled             = $true
        CleanPassesRequired = 1
        LockoutChainCap     = 2
        KnownTrapsEnabled   = $false
    }

    if (-not (Test-Path -LiteralPath $configPath -PathType Leaf)) {
        return [pscustomobject]$settings
    }

    $raw = Get-Content -LiteralPath $configPath -Raw -Encoding UTF8
    if ($raw -match '(?m)^\s*enabled:\s*(true|false)\s*$') {
        $settings.Enabled = ConvertTo-BoolLike $Matches[1]
    }
    if ($raw -match '(?m)^\s*clean_passes_required:\s*(\d+)\s*$') {
        $settings.CleanPassesRequired = [int]$Matches[1]
    }
    if ($raw -match '(?m)^\s*lockout_chain_cap:\s*(\d+)\s*$') {
        $settings.LockoutChainCap = [int]$Matches[1]
    }
    if ($raw -match '(?m)^\s*known_traps_integration:\s*(true|false)\s*$') {
        $settings.KnownTrapsEnabled = ConvertTo-BoolLike $Matches[1]
    }

    return [pscustomobject]$settings
}

function Get-ReviewerReasoningTiers {
    param([Parameter(Mandatory = $true)][string]$ProjectRoot)

    $configPath = Get-IterationConfigPath -ProjectRoot $ProjectRoot
    if (-not (Test-Path -LiteralPath $configPath -PathType Leaf)) {
        return @()
    }

    $lines = @(Get-Content -LiteralPath $configPath -Encoding UTF8)
    $tiers = New-Object System.Collections.Generic.List[object]
    $current = $null
    $section = $null
    $agentName = $null

    foreach ($line in $lines) {
        if ($line -match '^\s*reasoning_tiers:\s*$') {
            $section = 'reasoning_tiers'
            if ($null -ne $current) {
                $tiers.Add([pscustomobject]$current) | Out-Null
                $current = $null
            }
            continue
        }

        if ($line -match '^\s*agents:\s*$') {
            if ($null -ne $current -and $section -eq 'reasoning_tiers') {
                $tiers.Add([pscustomobject]$current) | Out-Null
            }
            $section = 'agents'
            $current = $null
            $agentName = $null
            continue
        }

        if ($section -eq 'reasoning_tiers') {
            if ($line -match '^\S' -and $line -notmatch '^\s*#') {
                if ($null -ne $current) {
                    $tiers.Add([pscustomobject]$current) | Out-Null
                }
                $current = $null
                $section = $null
            }
            elseif ($line -match '^\s*-\s+name:\s*(.+?)\s*$') {
                if ($null -ne $current) {
                    $tiers.Add([pscustomobject]$current) | Out-Null
                }
                $current = [ordered]@{
                    Name           = Normalize-YamlValue $Matches[1]
                    StrengthRank   = 0
                    ReviewerCapable = $true
                }
            }
            elseif ($null -ne $current -and $line -match '^\s+strength_rank:\s*(\d+)\s*$') {
                $current.StrengthRank = [int]$Matches[1]
            }
            elseif ($null -ne $current -and $line -match '^\s+reviewer_capable:\s*(true|false)\s*$') {
                $current.ReviewerCapable = ConvertTo-BoolLike $Matches[1]
            }
        }
        elseif ($section -eq 'agents') {
            if ($line -match '^\S' -and $line -notmatch '^\s*#') {
                if ($null -ne $current) {
                    $tiers.Add([pscustomobject]$current) | Out-Null
                }
                $current = $null
                $agentName = $null
                $section = $null
            }
            elseif ($line -match '^\s{2}([A-Za-z0-9_-]+):\s*$') {
                if ($null -ne $current) {
                    $tiers.Add([pscustomobject]$current) | Out-Null
                }
                $agentName = $Matches[1]
                $current = [ordered]@{
                    Name            = $agentName
                    StrengthRank    = 0
                    ReviewerCapable = $true
                    Enabled         = $false
                }
            }
            elseif ($null -ne $current -and $line -match '^\s{4}enabled:\s*(true|false)\s*$') {
                $current.Enabled = ConvertTo-BoolLike $Matches[1]
            }
            elseif ($null -ne $current -and $line -match '^\s{4}strength_rank:\s*(\d+)\s*$') {
                $current.StrengthRank = [int]$Matches[1]
            }
        }
    }

    if ($null -ne $current) {
        $tiers.Add([pscustomobject]$current) | Out-Null
    }

    $result = @($tiers.ToArray() | Where-Object {
            ($_ | Get-Member -Name Enabled -MemberType NoteProperty, AliasProperty, Property -ErrorAction SilentlyContinue) -eq $null -or
            $_.Enabled
        })
    return @($result | Sort-Object StrengthRank, Name)
}

function Get-ReviewerOwners {
    param([Parameter(Mandatory = $true)][string]$ProjectRoot)

    $assignmentsPath = Get-RoleAssignmentsPath -ProjectRoot $ProjectRoot
    if (-not (Test-Path -LiteralPath $assignmentsPath -PathType Leaf)) {
        return @()
    }

    $lines = @(Get-Content -LiteralPath $assignmentsPath -Encoding UTF8)
    $owners = New-Object System.Collections.Generic.List[object]
    $inRoles = $false
    $current = $null

    foreach ($line in $lines) {
        if ($line -match '^\s*roles:\s*$') {
            $inRoles = $true
            continue
        }

        if (-not $inRoles) {
            continue
        }

        if ($line -match '^\S' -and $line -notmatch '^\s*#') {
            if ($null -ne $current) {
                $owners.Add([pscustomobject]$current) | Out-Null
            }
            $current = $null
            $inRoles = $false
            continue
        }

        if ($line -match '^\s*-\s+(role|name):\s*(.+?)\s*$') {
            if ($null -ne $current) {
                $owners.Add([pscustomobject]$current) | Out-Null
            }
            $current = [ordered]@{
                RoleName       = Normalize-YamlValue $Matches[2]
                Owner          = $null
                ReasoningClass = $null
                Eligible       = $true
            }
            continue
        }

        if ($null -eq $current) {
            continue
        }

        if ($line -match '^\s+(role|name):\s*(.+?)\s*$') {
            $current.RoleName = Normalize-YamlValue $Matches[2]
        }
        elseif ($line -match '^\s+agent:\s*(.+?)\s*$') {
            $current.Owner = Normalize-YamlValue $Matches[1]
        }
        elseif ($line -match '^\s+assigned_to:\s*(.+?)\s*$') {
            $assignedTo = Normalize-YamlValue $Matches[1]
            if (-not [string]::IsNullOrWhiteSpace($assignedTo) -and $assignedTo -ne 'unassigned') {
                $current.Owner = $assignedTo
            }
        }
        elseif ($line -match '^\s+reasoning_class:\s*(.+?)\s*$') {
            $current.ReasoningClass = Normalize-YamlValue $Matches[1]
        }
        elseif ($line -match '^\s+preferred_agent:\s*(.+?)\s*$') {
            if ([string]::IsNullOrWhiteSpace($current.ReasoningClass)) {
                $current.ReasoningClass = Normalize-YamlValue $Matches[1]
            }
        }
        elseif ($line -match '^\s+eligible:\s*(true|false)\s*$') {
            $current.Eligible = ConvertTo-BoolLike $Matches[1]
        }
    }

    if ($null -ne $current) {
        $owners.Add([pscustomobject]$current) | Out-Null
    }

    return @($owners.ToArray() | Where-Object {
            $_.RoleName -eq 'Reviewer' -and
            $_.Eligible -and
            -not [string]::IsNullOrWhiteSpace($_.ReasoningClass)
        } | ForEach-Object {
            [pscustomobject]@{
                Owner          = if ([string]::IsNullOrWhiteSpace($_.Owner)) { $_.RoleName } else { $_.Owner }
                ReasoningClass = $_.ReasoningClass
            }
        })
}

function Get-ReviewerUsedOwners {
    param([Parameter(Mandatory = $true)][AllowEmptyCollection()][object[]]$Entries)

    $used = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
    foreach ($entry in @($Entries)) {
        $selectedReviewerOwner = $null
        if ($entry.RawText -match '(?m)^-\s+\*\*Selected Reviewer Owner\*\*:\s*`?(.+?)`?\s*$') {
            $selectedReviewerOwner = $Matches[1].Trim()
        }

        foreach ($value in @(
                $entry.PriorReviewerOwner,
                $entry.SameClassFallbackOwner,
                $entry.SelectedReviewerOwner,
                $selectedReviewerOwner
            )) {
            if (-not [string]::IsNullOrWhiteSpace([string]$value) -and $value -ne '(none)') {
                $null = $used.Add([string]$value)
            }
        }
    }

    return $used
}

function Get-TierRank {
    param(
        [Parameter(Mandatory = $true)][object[]]$Tiers,
        [Parameter(Mandatory = $true)][string]$TierName
    )

    $tier = @($Tiers | Where-Object { $_.Name -eq $TierName })[0]
    if ($null -eq $tier) {
        throw "Unknown reviewer reasoning class '$TierName'."
    }

    return [int]$tier.StrengthRank
}

function Select-ReviewerOwner {
    param(
        [Parameter(Mandatory = $true)][object[]]$Candidates,
        [Parameter(Mandatory = $true)][AllowEmptyCollection()][System.Collections.Generic.HashSet[string]]$UsedOwners,
        [string]$ExcludedOwner
    )

    $eligible = @($Candidates | Where-Object { $_.Owner -ne $ExcludedOwner })
    if ($eligible.Count -eq 0) {
        return $null
    }

    $unused = @($eligible | Where-Object { -not $UsedOwners.Contains($_.Owner) } | Sort-Object Owner)
    if ($unused.Count -gt 0) {
        return $unused[0]
    }

    return (@($eligible | Sort-Object Owner))[0]
}

function Resolve-ReviewerRouting {
    param(
        [Parameter(Mandatory = $true)][string]$ProjectRoot,
        [Parameter(Mandatory = $true)][string]$PriorReviewerClass,
        [Parameter(Mandatory = $true)][string]$PriorReviewerOwner,
        [Parameter(Mandatory = $true)][AllowEmptyCollection()][object[]]$ActiveEntries
    )

    $tiers = Get-ReviewerReasoningTiers -ProjectRoot $ProjectRoot
    $owners = Get-ReviewerOwners -ProjectRoot $ProjectRoot
    $usedOwners = Get-ReviewerUsedOwners -Entries $ActiveEntries
    if ($null -eq $usedOwners) {
        $usedOwners = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
    }
    $priorRank = Get-TierRank -Tiers $tiers -TierName $PriorReviewerClass

    $strongerTiers = @($tiers | Where-Object {
            $_.ReviewerCapable -and $_.StrengthRank -gt $priorRank
        } | Sort-Object StrengthRank, Name)

    foreach ($tier in $strongerTiers) {
        $candidates = @($owners | Where-Object { $_.ReasoningClass -eq $tier.Name })
        $selected = Select-ReviewerOwner -Candidates $candidates -UsedOwners $usedOwners -ExcludedOwner $PriorReviewerOwner
        if ($null -ne $selected) {
            return [pscustomobject]@{
                Status               = 'active'
                Action               = 'stronger-class'
                CurrentReviewerClass = $tier.Name
                CurrentReviewerOwner = $selected.Owner
                EscalatedToClass     = $tier.Name
                SameClassOwner       = $null
                Notes                = "Review rerouted to stronger reviewer class $($tier.Name) via $($selected.Owner)."
            }
        }
    }

    $sameClassCandidates = @($owners | Where-Object { $_.ReasoningClass -eq $PriorReviewerClass })
    $sameClassSelection = Select-ReviewerOwner -Candidates $sameClassCandidates -UsedOwners $usedOwners -ExcludedOwner $PriorReviewerOwner
    if ($null -ne $sameClassSelection) {
        return [pscustomobject]@{
            Status               = 'active'
            Action               = 'same-class-independent-owner'
            CurrentReviewerClass = $PriorReviewerClass
            CurrentReviewerOwner = $sameClassSelection.Owner
            EscalatedToClass     = $null
            SameClassOwner       = $sameClassSelection.Owner
            Notes                = "Review rerouted to independent reviewer owner $($sameClassSelection.Owner) at class $PriorReviewerClass."
        }
    }

    return [pscustomobject]@{
        Status               = 'held'
        Action               = 'human-direction-hold'
        CurrentReviewerClass = $PriorReviewerClass
        CurrentReviewerOwner = $null
        EscalatedToClass     = $PriorReviewerClass
        SameClassOwner       = $null
        Notes                = 'Awaiting explicit human direction before review continues.'
    }
}

function Get-NextReviewerRegressionEventId {
    param([Parameter(Mandatory = $true)][string]$ProjectRoot)

    $entries = Get-ReviewerRegressionLedgerEntries -ProjectRoot $ProjectRoot
    $nextNumber = 1
    if (@($entries).Count -gt 0) {
        $numbers = @($entries | ForEach-Object {
                if ($_.EventId -match '^RRE-(\d+)$') { [int]$Matches[1] }
            } | Where-Object { $null -ne $_ })
        if ($numbers.Count -gt 0) {
            $nextNumber = ($numbers | Measure-Object -Maximum).Maximum + 1
        }
    }

    return ('RRE-' + ([int]$nextNumber).ToString('000'))
}

function Initialize-ReviewerRegressionLedger {
    param([Parameter(Mandatory = $true)][string]$ProjectRoot)

    $ledgerPath = Get-ReviewerRegressionLedgerPath -ProjectRoot $ProjectRoot
    if (Test-Path -LiteralPath $ledgerPath -PathType Leaf) {
        return $ledgerPath
    }

    $now = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
    $content = @"
# Reviewer Regression Ledger
 
**Schema**: v1.0.0
**Created**: $now
**Source of Truth**: This append-only ledger is the authoritative record of all reviewer-regression events reported across all features and iterations in this repository.
 
## Purpose
 
This ledger records every concrete defect a human reports in a slice that a Squad reviewer previously approved or marked ready.
 
## Event Records
 
*No reviewer-regression events have been recorded yet.*
 
---
 
## Ledger Statistics
 
- **Total Events**: 0
- **Active Events**: 0
- **Resolved Events**: 0
- **Withdrawn Events**: 0
- **Strongest Escalation Ever Reached**: (none)
- **Last Updated**: $now
"@

    Write-Utf8FileAtomic -Path $ledgerPath -Content $content
    return $ledgerPath
}

function Update-ReviewerRegressionLedgerStatistics {
    param([Parameter(Mandatory = $true)][string]$ProjectRoot)

    $ledgerPath = Get-ReviewerRegressionLedgerPath -ProjectRoot $ProjectRoot
    if (-not (Test-Path -LiteralPath $ledgerPath -PathType Leaf)) {
        return
    }

    $entries = @(Get-ReviewerRegressionLedgerEntries -ProjectRoot $ProjectRoot)
    $lastUpdated = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
    $activeCount = @($entries | Where-Object { $_.EventStatus -eq 'active' }).Count
    $resolvedCount = @($entries | Where-Object { $_.EventStatus -eq 'resolved' }).Count
    $withdrawnCount = @($entries | Where-Object { $_.EventStatus -eq 'withdrawn' }).Count
    $priority = @{
        'none-yet'                    = 0
        'same-class-independent-owner' = 1
        'stronger-class'              = 2
        'human-direction-hold'        = 3
    }
    $strongest = '(none)'
    if ($entries.Count -gt 0) {
        $strongestEntry = @(
            $entries |
                Sort-Object `
                    @{ Expression = { $priority[[string]$_.EscalationAction] }; Descending = $true }, `
                    @{ Expression = { [string]$_.RecordedAt }; Descending = $true } |
                Select-Object -First 1
        )[0]
        $strongest = if ([string]::IsNullOrWhiteSpace([string]$strongestEntry.EscalationAction)) { '(none)' } else { $strongestEntry.EscalationAction }
    }

    Update-LockedFileContent -Path $ledgerPath -Transform {
        param($currentContent)

        $updated = $currentContent
        $replacements = [ordered]@{
            '(?m)^- \*\*Total Events\*\*: .+$'                     = "- **Total Events**: $($entries.Count)"
            '(?m)^- \*\*Active Events\*\*: .+$'                    = "- **Active Events**: $activeCount"
            '(?m)^- \*\*Resolved Events\*\*: .+$'                  = "- **Resolved Events**: $resolvedCount"
            '(?m)^- \*\*Withdrawn Events\*\*: .+$'                 = "- **Withdrawn Events**: $withdrawnCount"
            '(?m)^- \*\*Strongest Escalation Ever Reached\*\*: .+$' = "- **Strongest Escalation Ever Reached**: $strongest"
            '(?m)^- \*\*Last Updated\*\*: .+$'                     = "- **Last Updated**: $lastUpdated"
        }

        foreach ($pattern in $replacements.Keys) {
            if ($updated -match $pattern) {
                $updated = [regex]::Replace($updated, $pattern, $replacements[$pattern])
            }
        }

        return $updated
    } | Out-Null
}

function Add-ReviewerRegressionLedgerEntry {
    param(
        [Parameter(Mandatory = $true)][string]$ProjectRoot,
        [Parameter(Mandatory = $true)][hashtable]$Entry
    )

    $ledgerPath = Initialize-ReviewerRegressionLedger -ProjectRoot $ProjectRoot
    $markdownValue = {
        param([AllowNull()][string]$Value)

        if ([string]::IsNullOrWhiteSpace($Value)) {
            return '(none)'
        }

        return ('`{0}`' -f $Value)
    }

    $eventText = @(
        "### $($Entry.EventId)"
        ''
        "- **Feature**: $(& $markdownValue $Entry.Feature)"
        "- **Iteration**: $(& $markdownValue $Entry.Iteration)"
        "- **Slice**: $(& $markdownValue $Entry.Slice)"
        "- **Prior Reviewer Verdict**: $(& $markdownValue $Entry.PriorReviewerVerdict)"
        "- **Prior Reviewer Class**: $(& $markdownValue $Entry.PriorReviewerClass)"
        "- **Prior Reviewer Owner**: $(& $markdownValue $Entry.PriorReviewerOwner)"
        "- **Defect Description**: $(& $markdownValue $Entry.DefectDescription)"
        "- **Defect Source Location**: $(& $markdownValue $Entry.DefectSourceLocation)"
        "- **Event Status**: $(& $markdownValue $Entry.EventStatus)"
        "- **Severity**: $(& $markdownValue $Entry.Severity)"
        "- **Escalation Action**: $(& $markdownValue $Entry.EscalationAction)"
        "- **Escalated To Class**: $(& $markdownValue $Entry.EscalatedToClass)"
        "- **Selected Reviewer Owner**: $(& $markdownValue $Entry.SelectedReviewerOwner)"
        "- **Same-Class Fallback Owner**: $(& $markdownValue $Entry.SameClassFallbackOwner)"
        "- **Carry Forward Iteration**: $(& $markdownValue $Entry.CarryForwardIteration)"
        "- **Candidate Trap Status**: $(& $markdownValue $Entry.CandidateTrapStatus)"
        "- **Withdrawal Reference**: $(& $markdownValue $Entry.WithdrawalReference)"
        "- **De-Escalation Outcome**: $(& $markdownValue $Entry.DeEscalationOutcome)"
        "- **Recorded At**: $(& $markdownValue $Entry.RecordedAt)"
        ''
    ) -join [Environment]::NewLine

    Update-LockedFileContent -Path $ledgerPath -Transform {
        param($currentContent)

        $updated = $currentContent
        if ($updated -match '(?m)^\*No reviewer-regression events have been recorded yet\.\*$') {
            $updated = [regex]::Replace($updated, '(?m)^\*No reviewer-regression events have been recorded yet\.\*$', [System.Text.RegularExpressions.MatchEvaluator]{ param($m) $eventText.TrimEnd() })
        }
        elseif ($updated -match '(?m)^## Ledger Statistics\s*$') {
            $updated = [regex]::Replace($updated, '(?m)^## Ledger Statistics\s*$', [System.Text.RegularExpressions.MatchEvaluator]{ param($m) ($eventText + [Environment]::NewLine + '## Ledger Statistics') })
        }
        else {
            $updated = $updated.TrimEnd() + [Environment]::NewLine + [Environment]::NewLine + $eventText.TrimEnd() + [Environment]::NewLine
        }

        return $updated
    } | Out-Null

    Update-ReviewerRegressionLedgerStatistics -ProjectRoot $ProjectRoot
    return $ledgerPath
}

function Find-DuplicateReviewerRegressionEvent {
    param(
        [Parameter(Mandatory = $true)][string]$ProjectRoot,
        [Parameter(Mandatory = $true)][string]$Feature,
        [Parameter(Mandatory = $true)][string]$Slice,
        [Parameter(Mandatory = $true)][string]$DefectDescription,
        [Parameter(Mandatory = $true)][string]$DefectSourceLocation
    )

    return Get-ReviewerRegressionLedgerEntries -ProjectRoot $ProjectRoot |
        Where-Object {
            $_.Feature -eq $Feature -and
            $_.EventStatus -eq 'active' -and
            $_.Slice -eq $Slice -and
            $_.DefectDescription -eq $DefectDescription -and
            $_.DefectSourceLocation -eq $DefectSourceLocation
        } |
        Select-Object -First 1
}

function Get-ImplementerChainFromConfig {
    <#
    .SYNOPSIS
    Reads the implementer chain for a feature from .squad/config.json.
    #>

    param(
        [Parameter(Mandatory = $true)][string]$ProjectRoot,
        [Parameter(Mandatory = $true)][string]$Feature
    )

    $configPath = Join-Path $ProjectRoot '.squad\config.json'
    if (-not (Test-Path -LiteralPath $configPath -PathType Leaf)) {
        return @()
    }

    try {
        $config = Get-Content -LiteralPath $configPath -Raw -Encoding UTF8 | ConvertFrom-Json
        $featureKey = $Feature -replace '[/\\]', '_'
        
        if ($null -eq $config.reviewerRegressionState) {
            return @()
        }

        $featureState = $config.reviewerRegressionState.PSObject.Properties |
            Where-Object { $_.Name -eq $featureKey } |
            Select-Object -First 1

        if ($null -eq $featureState -or $null -eq $featureState.Value.implementerChain) {
            return @()
        }

        return @($featureState.Value.implementerChain)
    }
    catch {
        return @()
    }
}

function Update-ImplementerChainInConfig {
    <#
    .SYNOPSIS
    Updates the implementer chain for a feature in .squad/config.json.
    #>

    param(
        [Parameter(Mandatory = $true)][string]$ProjectRoot,
        [Parameter(Mandatory = $true)][string]$Feature,
        [Parameter(Mandatory = $true)][string]$ImplementerOwner
    )

    $configPath = Join-Path $ProjectRoot '.squad\config.json'
    $featureKey = $Feature -replace '[/\\]', '_'

    Update-LockedFileContent -Path $configPath -Transform {
        param($currentContent)

        $config = if ([string]::IsNullOrWhiteSpace($currentContent)) {
            [pscustomobject]@{
                version = '1.0'
                reviewerRegressionState = [pscustomobject]@{}
            }
        }
        else {
            $currentContent | ConvertFrom-Json
        }

        if ($null -eq $config.reviewerRegressionState) {
            $config | Add-Member -MemberType NoteProperty -Name 'reviewerRegressionState' -Value ([pscustomobject]@{})
        }

        $featureState = $config.reviewerRegressionState.PSObject.Properties |
            Where-Object { $_.Name -eq $featureKey } |
            Select-Object -First 1

        if ($null -eq $featureState) {
            $config.reviewerRegressionState | Add-Member -MemberType NoteProperty -Name $featureKey -Value ([pscustomobject]@{
                status = 'active'
                implementerChain = @($ImplementerOwner)
                lockoutChainLength = 1
                capActive = $false
            })
        }
        else {
            $chain = @($featureState.Value.implementerChain)
            if ($ImplementerOwner -notin $chain) {
                $chain += $ImplementerOwner
                $featureState.Value.implementerChain = $chain
                $featureState.Value.lockoutChainLength = $chain.Count
            }
        }

        return ($config | ConvertTo-Json -Depth 10)
    } | Out-Null
}

function Get-LockoutCapStatus {
    <#
    .SYNOPSIS
    Determines if the lockout-chain cap is active for a feature.
    #>

    param(
        [Parameter(Mandatory = $true)][string]$ProjectRoot,
        [Parameter(Mandatory = $true)][string]$Feature,
        [Parameter(Mandatory = $true)][int]$LockoutChainCap
    )

    $chain = @(Get-ImplementerChainFromConfig -ProjectRoot $ProjectRoot -Feature $Feature)
    $chainLength = $chain.Count
    $capThreshold = 1 + $LockoutChainCap  # original + cap rotations
    $capActive = $chainLength -ge $capThreshold

    return [pscustomobject]@{
        ImplementerChain = $chain
        ChainLength = $chainLength
        CapThreshold = $capThreshold
        CapActive = $capActive
    }
}

function Get-IterationReference {
    param([string]$IterationDirectory)

    if ([string]::IsNullOrWhiteSpace($IterationDirectory)) {
        return $null
    }

    return Split-Path -Leaf $IterationDirectory
}

function Get-FeatureReferenceFromIterationDirectory {
    param([string]$ProjectRoot, [string]$IterationDirectory)

    if ([string]::IsNullOrWhiteSpace($IterationDirectory)) {
        return $null
    }

    $normalizedProject = [System.IO.Path]::GetFullPath($ProjectRoot)
    $normalizedIteration = [System.IO.Path]::GetFullPath($IterationDirectory)
    $relative = [System.IO.Path]::GetRelativePath($normalizedProject, $normalizedIteration)
    $relative = $relative -replace '\\', '/'
    if ($relative -match '^(specs/[^/]+)/iterations/[^/]+$') {
        return $Matches[1]
    }

    return $null
}

function Get-ReviewerRegressionReadback {
    param(
        [Parameter(Mandatory = $true)][string]$ProjectRoot,
        [Parameter(Mandatory = $true)][string]$Feature
    )

    $settings = Get-ReviewerRegressionSettings -ProjectRoot $ProjectRoot
    $chain = Get-ActiveReviewerRegressionChain -ProjectRoot $ProjectRoot -Feature $Feature
    $entries = @(
        Get-ReviewerRegressionLedgerEntries -ProjectRoot $ProjectRoot |
            Where-Object { $_.Feature -eq $Feature -and $_.EventStatus -eq 'active' }
    )

    $currentOwner = $chain.CurrentReviewerOwner
    if ([string]::IsNullOrWhiteSpace([string]$currentOwner)) {
        $selectedOwner = $entries |
            Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_.SelectedReviewerOwner) } |
            Select-Object -Last 1
        if ($null -ne $selectedOwner) {
            $currentOwner = $selectedOwner.SelectedReviewerOwner
        }
    }

    $notes = switch ([string]$chain.StrongestUnresolvedAction) {
        'human-direction-hold' { 'Awaiting explicit human direction before review continues.'; break }
        'same-class-independent-owner' {
            if ([string]::IsNullOrWhiteSpace([string]$currentOwner)) {
                "Review rerouted to an independent reviewer owner at class $($chain.CurrentReviewerClass)."
            }
            else {
                "Review rerouted to independent reviewer owner $currentOwner at class $($chain.CurrentReviewerClass)."
            }
            break
        }
        'stronger-class' {
            if ([string]::IsNullOrWhiteSpace([string]$currentOwner)) {
                "Review rerouted to stronger reviewer class $($chain.CurrentReviewerClass)."
            }
            else {
                "Review rerouted to stronger reviewer class $($chain.CurrentReviewerClass) via $currentOwner."
            }
            break
        }
        default { $null }
    }

    $lastEvent = $entries | Sort-Object RecordedAt | Select-Object -Last 1

    $capStatus = Get-LockoutCapStatus -ProjectRoot $ProjectRoot -Feature $Feature -LockoutChainCap $settings.LockoutChainCap
    $capNotes = if ($capStatus.CapActive) {
        "Lockout-chain cap active ($($capStatus.ChainLength) implementers = original + $($settings.LockoutChainCap) rotations). Further rotation blocked. Human-owned revision or approved alternate owner required per FR-010."
    }
    else {
        $null
    }

    $combinedNotes = if ($notes -and $capNotes) {
        "$notes $capNotes"
    }
    elseif ($capNotes) {
        $capNotes
    }
    else {
        $notes
    }

    $lockedOutAgents = if ($capStatus.CapActive) {
        @('Standard implementer rotation pool (original + 2 rotations exhausted)')
    }
    else {
        @()
    }

    $nextOwnerPath = if ($capStatus.CapActive) {
        'Awaiting human-owned revision or explicitly approved alternate owner recorded in `.squad/decisions.md`'
    }
    else {
        $null
    }

    $finalNotes = if ($combinedNotes -and $nextOwnerPath) {
        "$combinedNotes Next Owner Path: $nextOwnerPath"
    }
    elseif ($nextOwnerPath) {
        "Next Owner Path: $nextOwnerPath"
    }
    else {
        $combinedNotes
    }

    return [pscustomobject]@{
        Status                    = $chain.Status
        Feature                   = $Feature
        ActiveEventIds            = @($chain.ActiveEventIds)
        StrongestUnresolvedAction = $chain.StrongestUnresolvedAction
        CurrentReviewerClass      = $chain.CurrentReviewerClass
        PriorReviewerClass        = $chain.PriorReviewerClass
        CurrentReviewerOwner      = $currentOwner
        CleanPassesRequired       = $settings.CleanPassesRequired
        CleanPassesObserved       = $chain.CleanPassesObserved
        LockoutCap                = $settings.LockoutChainCap
        LockoutChainLength        = $capStatus.ChainLength
        ImplementerChain          = $capStatus.ImplementerChain
        CapActive                 = $capStatus.CapActive
        LockedOutAgents           = $lockedOutAgents
        NextOwnerPath             = $nextOwnerPath
        CarryForwardFromIteration = $chain.CarryForwardFromIteration
        LastEvent                 = if ($null -ne $lastEvent) { $lastEvent.RecordedAt } else { $null }
        Notes                     = $finalNotes
    }
}

function Set-ReviewerRegressionStateBlock {
    param(
        [Parameter(Mandatory = $true)][string]$IterationDirectory,
        [Parameter(Mandatory = $true)][object]$Chain
    )

    $statePath = Join-Path $IterationDirectory 'state.md'
    if (-not (Test-Path -LiteralPath $statePath -PathType Leaf)) {
        throw "Missing iteration state file: $statePath"
    }

    $blockLines = @(
        '<!-- >>> specrew-managed reviewer-regression-state >>> -->'
        '## Reviewer Regression State'
        ''
        "- **Status**: $(if ([string]::IsNullOrWhiteSpace([string]$Chain.Status)) { 'inactive' } else { $Chain.Status })"
        "- **Feature**: $(if ([string]::IsNullOrWhiteSpace([string]$Chain.Feature)) { '(none)' } else { $Chain.Feature })"
        "- **Active Event IDs**: $(if (@($Chain.ActiveEventIds).Count -gt 0) { (@($Chain.ActiveEventIds) -join ', ') } else { '(none)' })"
        "- **Prior Reviewer Class**: $(if ([string]::IsNullOrWhiteSpace([string]$Chain.PriorReviewerClass)) { '(none)' } else { $Chain.PriorReviewerClass })"
        "- **Current Reviewer Class**: $(if ([string]::IsNullOrWhiteSpace([string]$Chain.CurrentReviewerClass)) { '(none)' } else { $Chain.CurrentReviewerClass })"
        "- **Current Reviewer Owner**: $(if ([string]::IsNullOrWhiteSpace([string]$Chain.CurrentReviewerOwner)) { '(none)' } else { $Chain.CurrentReviewerOwner })"
        "- **Lockout Chain Length**: $([int]$Chain.LockoutChainLength)"
        "- **Lockout Cap**: $([int]$Chain.LockoutCap)"
        "- **Cap Active**: $([string]([bool]$Chain.CapActive).ToString().ToLowerInvariant())"
        "- **Locked Out Agents**: $(if (@($Chain.LockedOutAgents).Count -gt 0) { (@($Chain.LockedOutAgents) -join ', ') } else { '(none)' })"
        "- **Carry Forward From Iteration**: $(if ([string]::IsNullOrWhiteSpace([string]$Chain.CarryForwardFromIteration)) { '(none)' } else { $Chain.CarryForwardFromIteration })"
        "- **Last Event**: $(if ([string]::IsNullOrWhiteSpace([string]$Chain.LastEvent)) { '(none)' } else { $Chain.LastEvent })"
        "- **Notes**: $(if ([string]::IsNullOrWhiteSpace([string]$Chain.Notes)) { '(none)' } else { $Chain.Notes })"
        '<!-- <<< specrew-managed reviewer-regression-state <<< -->'
    )
    $blockText = $blockLines -join [Environment]::NewLine

    Update-LockedFileContent -Path $statePath -Transform {
        param($currentContent)

        if ($currentContent -match '(?s)<!-- >>> specrew-managed reviewer-regression-state >>> -->.*?<!-- <<< specrew-managed reviewer-regression-state <<< -->') {
            return [regex]::Replace(
                $currentContent,
                '(?s)<!-- >>> specrew-managed reviewer-regression-state >>> -->.*?<!-- <<< specrew-managed reviewer-regression-state <<< -->',
                [System.Text.RegularExpressions.MatchEvaluator]{ param($m) $blockText }
            )
        }

        if ($currentContent -match '(?m)^## Task Status\s*$') {
            return [regex]::Replace(
                $currentContent,
                '(?m)^## Task Status\s*$',
                [System.Text.RegularExpressions.MatchEvaluator]{ param($m) ($blockText + [Environment]::NewLine + [Environment]::NewLine + '## Task Status') }
            )
        }

        return $currentContent.TrimEnd() + [Environment]::NewLine + [Environment]::NewLine + $blockText + [Environment]::NewLine
    } | Out-Null

    return $statePath
}

function Require-ParameterValue {
    param(
        [Parameter(Mandatory = $true)][string]$Name,
        [AllowNull()][string]$Value
    )

    if ([string]::IsNullOrWhiteSpace($Value)) {
        throw "Parameter -$Name is required for '$Mode' mode."
    }
}

switch ($Mode) {
    'get' {
        if ([string]::IsNullOrWhiteSpace($Feature) -and -not [string]::IsNullOrWhiteSpace($IterationDirectory)) {
            $Feature = Get-FeatureReferenceFromIterationDirectory -ProjectRoot $ProjectRoot -IterationDirectory $IterationDirectory
        }
        Require-ParameterValue -Name 'Feature' -Value $Feature

        Get-ReviewerRegressionReadback -ProjectRoot $ProjectRoot -Feature $Feature | ConvertTo-Json -Depth 8
        break
    }
    'project' {
        if ([string]::IsNullOrWhiteSpace($Feature) -and -not [string]::IsNullOrWhiteSpace($IterationDirectory)) {
            $Feature = Get-FeatureReferenceFromIterationDirectory -ProjectRoot $ProjectRoot -IterationDirectory $IterationDirectory
        }
        Require-ParameterValue -Name 'Feature' -Value $Feature
        Require-ParameterValue -Name 'IterationDirectory' -Value $IterationDirectory

        $chain = Get-ReviewerRegressionReadback -ProjectRoot $ProjectRoot -Feature $Feature
        $null = Set-ReviewerRegressionStateBlock -IterationDirectory $IterationDirectory -Chain $chain
        $chain | ConvertTo-Json -Depth 8
        break
    }
    'report' {
        foreach ($required in @(
                'Feature',
                'IterationDirectory',
                'Slice',
                'PriorReviewerVerdict',
                'PriorReviewerClass',
                'PriorReviewerOwner',
                'DefectDescription',
                'DefectSourceLocation'
            )) {
            Require-ParameterValue -Name $required -Value (Get-Variable -Name $required -ValueOnly)
        }

        $existing = Find-DuplicateReviewerRegressionEvent -ProjectRoot $ProjectRoot -Feature $Feature -Slice $Slice -DefectDescription $DefectDescription -DefectSourceLocation $DefectSourceLocation
        if ($null -ne $existing) {
            $chain = Get-ReviewerRegressionReadback -ProjectRoot $ProjectRoot -Feature $Feature
            $null = Set-ReviewerRegressionStateBlock -IterationDirectory $IterationDirectory -Chain $chain
            [pscustomobject]@{
                EventId    = $existing.EventId
                Duplicate  = $true
                Routing    = [pscustomobject]@{
                    Status               = $chain.Status
                    Action               = $chain.StrongestUnresolvedAction
                    CurrentReviewerClass = $chain.CurrentReviewerClass
                    CurrentReviewerOwner = $chain.CurrentReviewerOwner
                    Notes                = $chain.Notes
                }
                Chain      = $chain
                LedgerPath = Get-ReviewerRegressionLedgerPath -ProjectRoot $ProjectRoot
            } | ConvertTo-Json -Depth 8
            break
        }

        $activeEntries = @(
            Get-ReviewerRegressionLedgerEntries -ProjectRoot $ProjectRoot |
                Where-Object { $_.Feature -eq $Feature -and $_.EventStatus -eq 'active' }
        )
        $routing = Resolve-ReviewerRouting -ProjectRoot $ProjectRoot -PriorReviewerClass $PriorReviewerClass -PriorReviewerOwner $PriorReviewerOwner -ActiveEntries $activeEntries
        $recordedAt = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
        $newEventId = Get-NextReviewerRegressionEventId -ProjectRoot $ProjectRoot
        $entry = [ordered]@{
            EventId                = $newEventId
            Feature                = $Feature
            Iteration              = Get-IterationReference -IterationDirectory $IterationDirectory
            Slice                  = $Slice
            PriorReviewerVerdict   = $PriorReviewerVerdict
            PriorReviewerClass     = $PriorReviewerClass
            PriorReviewerOwner     = $PriorReviewerOwner
            DefectDescription      = $DefectDescription
            DefectSourceLocation   = $DefectSourceLocation
            EventStatus            = 'active'
            Severity               = 'soft-warning'
            EscalationAction       = $routing.Action
            EscalatedToClass       = $routing.EscalatedToClass
            SelectedReviewerOwner  = $routing.CurrentReviewerOwner
            SameClassFallbackOwner = $routing.SameClassOwner
            CarryForwardIteration  = $null
            CandidateTrapStatus    = 'not-applicable'
            WithdrawalReference    = $null
            DeEscalationOutcome    = $null
            RecordedAt             = $recordedAt
        }

        # T026: FR-014 - Detect closed iterations and mark carry-forward
        $iterationStatePath = Join-Path $IterationDirectory 'state.md'
        if (Test-Path -LiteralPath $iterationStatePath -PathType Leaf) {
            $stateContent = Get-Content -LiteralPath $iterationStatePath -Raw -Encoding UTF8
            $isClosedPattern = '\*\*Status\*\*:\s+(complete|closed)'
            if ($stateContent -match $isClosedPattern) {
                # Iteration is closed; find next iteration number
                $currentIterNum = [regex]::Match((Get-IterationReference -IterationDirectory $IterationDirectory), '\d+').Value
                if (-not [string]::IsNullOrWhiteSpace($currentIterNum)) {
                    $nextIterNum = ([int]$currentIterNum + 1).ToString('000')
                    $entry.CarryForwardIteration = "iteration $nextIterNum"
                }
            }
        }

        # T025: FR-012 - Conditional candidate-trap proposal when corpus is enabled
        $settings = Get-ReviewerRegressionSettings -ProjectRoot $ProjectRoot
        $candidateTrapProposed = $false
        if ($settings.KnownTrapsEnabled) {
            $knownTrapsPath = Join-Path $ProjectRoot '.specrew\quality\known-traps.md'
            $knownTrapsDir = Split-Path -Parent $knownTrapsPath
            if (-not (Test-Path -LiteralPath $knownTrapsDir -PathType Container)) {
                $null = New-Item -ItemType Directory -Path $knownTrapsDir -Force
            }
            
            if (-not (Test-Path -LiteralPath $knownTrapsPath -PathType Leaf)) {
                # Initialize known-traps file if it doesn't exist
                [System.IO.File]::WriteAllText($knownTrapsPath, @"
# Known Traps
 
**Schema**: v1
**Last Updated**: $(Get-Date -Format 'yyyy-MM-dd')
 
## Trap Catalog
 
<!-- Approved traps and candidate traps are recorded below -->
 
"@
, [System.Text.UTF8Encoding]::new($false))
            }
            
            # Propose candidate trap
            $candidateTrapEntry = @"
 
<!-- candidate-trap-from-event: $newEventId -->
## Candidate Trap (unapproved): $newEventId
 
- **Source Event**: $newEventId
- **Feature**: $Feature
- **Defect Description**: $DefectDescription
- **Defect Source Location**: $DefectSourceLocation
- **Pattern**: _(Awaiting human review and pattern extraction)_
- **Detection**: _(Awaiting lens or mechanical check definition)_
- **Status**: candidate
 
**Review Notes**: This candidate trap was automatically proposed from reviewer-regression event $newEventId. A human reviewer should extract the defect pattern, define detection logic, and approve or reject this trap entry.
 
<!-- end-candidate-trap -->
"@

            
            Update-LockedFileContent -Path $knownTrapsPath -Transform {
                param($currentContent)
                
                # Append candidate trap before the final empty line or at end
                $updated = $currentContent.TrimEnd() + [Environment]::NewLine + $candidateTrapEntry.TrimEnd() + [Environment]::NewLine
                return $updated
            } | Out-Null
            
            $candidateTrapProposed = $true
            $entry.CandidateTrapStatus = 'proposed-awaiting-approval'
        }

        $ledgerPath = Add-ReviewerRegressionLedgerEntry -ProjectRoot $ProjectRoot -Entry $entry

        # Track implementer owner and check for cap activation (T017: FR-009, FR-010)
        $capActivatedNow = $false
        if (-not [string]::IsNullOrWhiteSpace($ImplementerOwner)) {
            $capStatusBefore = Get-LockoutCapStatus -ProjectRoot $ProjectRoot -Feature $Feature -LockoutChainCap (Get-ReviewerRegressionSettings -ProjectRoot $ProjectRoot).LockoutChainCap
            Update-ImplementerChainInConfig -ProjectRoot $ProjectRoot -Feature $Feature -ImplementerOwner $ImplementerOwner
            $capStatusAfter = Get-LockoutCapStatus -ProjectRoot $ProjectRoot -Feature $Feature -LockoutChainCap (Get-ReviewerRegressionSettings -ProjectRoot $ProjectRoot).LockoutChainCap
            
            if (-not $capStatusBefore.CapActive -and $capStatusAfter.CapActive) {
                $capActivatedNow = $true
            }
        }

        # Record cap activation decision (T018: FR-010, FR-011)
        $capDecisionPath = $null
        if ($capActivatedNow) {
            $capChain = @(Get-ImplementerChainFromConfig -ProjectRoot $ProjectRoot -Feature $Feature)
            $capDecisionPath = Add-StructuredDecisionsLedgerEntry -ProjectRoot $ProjectRoot -Title "Lockout-chain cap activated for $Feature" -Type 'lockout-cap' -AffectedRequirement 'FR-009, FR-010, FR-011' -AffectedIteration (Get-IterationReference -IterationDirectory $IterationDirectory) -NextAction 'awaiting-human-owned-revision-or-approved-alternate' -Rationale "Implementer lockout-chain reached the configured cap ($(Get-ReviewerRegressionSettings -ProjectRoot $ProjectRoot).LockoutChainCap rotations beyond original implementer). Cap is now active." -DetailLines @(
                "- **Feature**: $Feature"
                "- **Implementer Chain**: $($capChain -join ' → ')"
                "- **Chain Length**: $($capChain.Count)"
                "- **Cap Threshold**: $(1 + (Get-ReviewerRegressionSettings -ProjectRoot $ProjectRoot).LockoutChainCap) (original + cap rotations)"
                "- **Cap State**: active"
                "- **Next Owner Path**: Awaiting human-owned revision or approved alternate owner recorded in ``.squad/decisions.md``"
            )
        }

        $decisionPath = Add-StructuredDecisionsLedgerEntry -ProjectRoot $ProjectRoot -Title "Reviewer regression $newEventId" -Type 'reviewer-regression-escalation' -AffectedRequirement 'FR-001, FR-002, FR-003, FR-004, FR-015' -AffectedIteration (Get-IterationReference -IterationDirectory $IterationDirectory) -NextAction 'continue-review-routing' -Rationale $routing.Notes -DetailLines @(
            "- **Event ID**: $newEventId"
            "- **Feature**: $Feature"
            "- **Routing Outcome**: $($routing.Action)"
            "- **Selected Reviewer Class**: $(if ([string]::IsNullOrWhiteSpace([string]$routing.CurrentReviewerClass)) { '(none)' } else { $routing.CurrentReviewerClass })"
            "- **Selected Reviewer Owner**: $(if ([string]::IsNullOrWhiteSpace([string]$routing.CurrentReviewerOwner)) { '(none)' } else { $routing.CurrentReviewerOwner })"
            "- **Hold Active**: $([string]($routing.Status -eq 'held').ToString().ToLowerInvariant())"
        )

        $chain = Get-ReviewerRegressionReadback -ProjectRoot $ProjectRoot -Feature $Feature
        $null = Set-ReviewerRegressionStateBlock -IterationDirectory $IterationDirectory -Chain $chain

        [pscustomobject]@{
            EventId               = $newEventId
            Duplicate             = $false
            Routing               = [pscustomobject]@{
                Status               = $routing.Status
                Action               = $routing.Action
                CurrentReviewerClass = $routing.CurrentReviewerClass
                CurrentReviewerOwner = $routing.CurrentReviewerOwner
                Notes                = $routing.Notes
            }
            Chain                 = $chain
            LedgerPath            = $ledgerPath
            DecisionPath          = $decisionPath
            CapActivated          = $capActivatedNow
            CapDecisionPath       = $capDecisionPath
            CandidateTrapProposed = $candidateTrapProposed
            CarryForwardIteration = $entry.CarryForwardIteration
        } | ConvertTo-Json -Depth 8
        break
    }
    'resolve' {
        # T024: FR-005 clean-pass de-escalation
        if ([string]::IsNullOrWhiteSpace($Feature) -and -not [string]::IsNullOrWhiteSpace($IterationDirectory)) {
            $Feature = Get-FeatureReferenceFromIterationDirectory -ProjectRoot $ProjectRoot -IterationDirectory $IterationDirectory
        }
        Require-ParameterValue -Name 'Feature' -Value $Feature
        Require-ParameterValue -Name 'IterationDirectory' -Value $IterationDirectory

        $activeEntries = @(
            Get-ReviewerRegressionLedgerEntries -ProjectRoot $ProjectRoot |
                Where-Object { $_.Feature -eq $Feature -and $_.EventStatus -eq 'active' }
        )

        if ($activeEntries.Count -eq 0) {
            # No active events to resolve
            $chain = Get-ReviewerRegressionReadback -ProjectRoot $ProjectRoot -Feature $Feature
            [pscustomobject]@{
                Resolved     = $false
                EventIds     = @()
                Message      = "No active reviewer-regression events for feature $Feature"
                Chain        = $chain
            } | ConvertTo-Json -Depth 8
            break
        }

        # Mark all active events as resolved in ledger
        $ledgerPath = Get-ReviewerRegressionLedgerPath -ProjectRoot $ProjectRoot
        $resolvedAt = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
        
        Update-LockedFileContent -Path $ledgerPath -Transform {
            param($currentContent)
            
            $updated = $currentContent
            foreach ($entry in $activeEntries) {
                # Update EventStatus to resolved
                $pattern = "(?ms)(### $([regex]::Escape($entry.EventId)).*?-\s+\*\*Event Status\*\*:\s*`)active(`.*?)(###|\z)"
                $updated = [regex]::Replace($updated, $pattern, {
                    param($m)
                    $before = $m.Groups[1].Value
                    $after = $m.Groups[2].Value
                    $next = $m.Groups[3].Value
                    "$before`resolved$after$next"
                })
                
                # Update De-Escalation Outcome
                $pattern = "(?ms)(### $([regex]::Escape($entry.EventId)).*?-\s+\*\*De-Escalation Outcome\*\*:\s*`)\(none\)(`.*?)(###|\z)"
                $updated = [regex]::Replace($updated, $pattern, {
                    param($m)
                    $before = $m.Groups[1].Value
                    $after = $m.Groups[2].Value
                    $next = $m.Groups[3].Value
                    "$before`clean-pass at $resolvedAt$after$next"
                })
            }
            
            return $updated
        } | Out-Null

        # Clear runtime state for this feature
        $configPath = Join-Path $ProjectRoot '.squad\config.json'
        $featureKey = $Feature -replace '[/\\]', '_'
        
        Update-LockedFileContent -Path $configPath -Transform {
            param($currentContent)
            
            $config = if ([string]::IsNullOrWhiteSpace($currentContent)) {
                [pscustomobject]@{
                    version = '1.0'
                    reviewerRegressionState = [pscustomobject]@{}
                }
            }
            else {
                $currentContent | ConvertFrom-Json
            }
            
            if ($null -ne $config.reviewerRegressionState.PSObject.Properties[$featureKey]) {
                $config.reviewerRegressionState.PSObject.Properties.Remove($featureKey)
            }
            
            return ($config | ConvertTo-Json -Depth 10)
        } | Out-Null

        # Update state.md managed block
        $chain = Get-ReviewerRegressionReadback -ProjectRoot $ProjectRoot -Feature $Feature
        $null = Set-ReviewerRegressionStateBlock -IterationDirectory $IterationDirectory -Chain $chain

        [pscustomobject]@{
            Resolved     = $true
            EventIds     = @($activeEntries | ForEach-Object { $_.EventId })
            Message      = "Resolved $($activeEntries.Count) active event(s) via clean pass"
            Chain        = $chain
        } | ConvertTo-Json -Depth 8
        break
    }
    'withdraw' {
        # T024: FR-008 withdrawal reversal
        Require-ParameterValue -Name 'EventId' -Value $EventId
        if ([string]::IsNullOrWhiteSpace($Feature) -and -not [string]::IsNullOrWhiteSpace($IterationDirectory)) {
            $Feature = Get-FeatureReferenceFromIterationDirectory -ProjectRoot $ProjectRoot -IterationDirectory $IterationDirectory
        }
        Require-ParameterValue -Name 'Feature' -Value $Feature
        Require-ParameterValue -Name 'IterationDirectory' -Value $IterationDirectory

        $targetEvent = Get-ReviewerRegressionLedgerEntries -ProjectRoot $ProjectRoot |
            Where-Object { $_.EventId -eq $EventId } |
            Select-Object -First 1

        if ($null -eq $targetEvent) {
            throw "Event $EventId not found in reviewer-regression ledger."
        }

        if ($targetEvent.EventStatus -ne 'active') {
            # Already withdrawn or resolved - idempotent no-op
            $chain = Get-ReviewerRegressionReadback -ProjectRoot $ProjectRoot -Feature $Feature
            [pscustomobject]@{
                EventId    = $EventId
                Withdrawn  = $false
                Message    = "Event $EventId is not active (status: $($targetEvent.EventStatus)); no withdrawal needed"
                Chain      = $chain
            } | ConvertTo-Json -Depth 8
            break
        }

        # Mark event as withdrawn in ledger
        $ledgerPath = Get-ReviewerRegressionLedgerPath -ProjectRoot $ProjectRoot
        $withdrawnAt = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
        
        Update-LockedFileContent -Path $ledgerPath -Transform {
            param($currentContent)
            
            # Update EventStatus to withdrawn
            $pattern = "(?ms)(### $([regex]::Escape($EventId)).*?-\s+\*\*Event Status\*\*:\s*`)active(`.*?)(###|\z)"
            $updated = [regex]::Replace($currentContent, $pattern, {
                param($m)
                $before = $m.Groups[1].Value
                $after = $m.Groups[2].Value
                $next = $m.Groups[3].Value
                "$before`withdrawn$after$next"
            })
            
            # Update Withdrawal Reference
            $pattern = "(?ms)(### $([regex]::Escape($EventId)).*?-\s+\*\*Withdrawal Reference\*\*:\s*`)\(none\)(`.*?)(###|\z)"
            $updated = [regex]::Replace($updated, $pattern, {
                param($m)
                $before = $m.Groups[1].Value
                $after = $m.Groups[2].Value
                $next = $m.Groups[3].Value
                "$before`misreport-withdrawn at $withdrawnAt$after$next"
            })
            
            return $updated
        } | Out-Null

        # T025: Clean up unapproved candidate traps if corpus is enabled
        $settings = Get-ReviewerRegressionSettings -ProjectRoot $ProjectRoot
        if ($settings.KnownTrapsEnabled) {
            $knownTrapsPath = Join-Path $ProjectRoot '.specrew\quality\known-traps.md'
            if (Test-Path -LiteralPath $knownTrapsPath -PathType Leaf) {
                Update-LockedFileContent -Path $knownTrapsPath -Transform {
                    param($currentContent)
                    
                    # Remove any unapproved trap entries referencing this event
                    $pattern = "(?ms)<!-- candidate-trap-from-event: $([regex]::Escape($EventId)) -->.*?<!-- end-candidate-trap -->\s*"
                    $updated = [regex]::Replace($currentContent, $pattern, '')
                    
                    return $updated
                } | Out-Null
            }
        }

        # Check if there are remaining active events for this feature
        $remainingActive = @(
            Get-ReviewerRegressionLedgerEntries -ProjectRoot $ProjectRoot |
                Where-Object { $_.Feature -eq $Feature -and $_.EventStatus -eq 'active' -and $_.EventId -ne $EventId }
        )

        if ($remainingActive.Count -eq 0) {
            # No more active events - clear runtime state
            $configPath = Join-Path $ProjectRoot '.squad\config.json'
            $featureKey = $Feature -replace '[/\\]', '_'
            
            Update-LockedFileContent -Path $configPath -Transform {
                param($currentContent)
                
                $config = if ([string]::IsNullOrWhiteSpace($currentContent)) {
                    [pscustomobject]@{
                        version = '1.0'
                        reviewerRegressionState = [pscustomobject]@{}
                    }
                }
                else {
                    $currentContent | ConvertFrom-Json
                }
                
                if ($null -ne $config.reviewerRegressionState.PSObject.Properties[$featureKey]) {
                    $config.reviewerRegressionState.PSObject.Properties.Remove($featureKey)
                }
                
                return ($config | ConvertTo-Json -Depth 10)
            } | Out-Null
        }

        # Update state.md managed block
        $chain = Get-ReviewerRegressionReadback -ProjectRoot $ProjectRoot -Feature $Feature
        $null = Set-ReviewerRegressionStateBlock -IterationDirectory $IterationDirectory -Chain $chain

        [pscustomobject]@{
            EventId    = $EventId
            Withdrawn  = $true
            Message    = "Event $EventId withdrawn; state reverted"
            Chain      = $chain
        } | ConvertTo-Json -Depth 8
        break
    }
}