extensions/specrew-speckit/scripts/manage-escalation-state.ps1

[CmdletBinding()]
param(
    [Parameter(Mandatory = $true)]
    [string]$IterationDirectory,

    [ValidateSet('get', 'activate', 'resolve', 'clear')]
    [string]$Mode = 'get',

    [string]$Artifact,
    [string]$Gate,
    [string]$Owner,
    [string[]]$LockedOutAgents,
    [int]$FailureCount,

    [ValidateSet('efficiency', 'balanced', 'deep')]
    [string]$Tier,

    [string]$Notes,
    [switch]$DryRun,
    [switch]$PassThru
)

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

$sharedGovernancePath = Join-Path $PSScriptRoot 'shared-governance.ps1'
if (-not (Test-Path -LiteralPath $sharedGovernancePath -PathType Leaf)) {
    throw "Missing shared governance helper '$sharedGovernancePath'."
}
. $sharedGovernancePath

function Resolve-ProjectRoot {
    param([string]$StartPath)

    $current = [System.IO.DirectoryInfo]::new([System.IO.Path]::GetFullPath($StartPath))
    while ($null -ne $current) {
        if ((Test-Path -LiteralPath (Join-Path $current.FullName '.squad') -PathType Container) -or
            (Test-Path -LiteralPath (Join-Path $current.FullName '.specrew') -PathType Container)) {
            return $current.FullName
        }

        $current = $current.Parent
    }

    throw "Could not resolve project root from '$StartPath'."
}

function Test-IsNullish {
    param([AllowNull()][string]$Value)

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

    return $Value.Trim() -match '^(?:—|-|none|null|n/a|\(none\)|blank)$'
}

function Get-DefaultEscalationState {
    return [pscustomobject]@{
        status            = 'inactive'
        artifact          = $null
        gate              = $null
        failure_count     = 0
        current_tier      = 'efficiency'
        current_owner     = $null
        locked_out_agents = @()
        last_escalated    = $null
        resolved_at       = $null
        notes             = $null
    }
}

function Get-ManagedBlockContent {
    param(
        [string]$Content,
        [string]$BlockName
    )

    $startMarker = "<!-- >>> specrew-managed $BlockName >>> -->"
    $endMarker = "<!-- <<< specrew-managed $BlockName <<< -->"
    $pattern = '(?ms)' + [regex]::Escape($startMarker) + '\s*(.*?)\s*' + [regex]::Escape($endMarker)
    $match = [regex]::Match($Content, $pattern)
    if (-not $match.Success) {
        return $null
    }

    return $match.Groups[1].Value.Trim()
}

function Set-ManagedBlock {
    param(
        [string]$Content,
        [string]$BlockName,
        [string]$BlockContent
    )

    $startMarker = "<!-- >>> specrew-managed $BlockName >>> -->"
    $endMarker = "<!-- <<< specrew-managed $BlockName <<< -->"
    $managedBlock = @(
        $startMarker
        $BlockContent.Trim()
        $endMarker
    ) -join [Environment]::NewLine
    $pattern = '(?ms)\s*' + [regex]::Escape($startMarker) + '.*?' + [regex]::Escape($endMarker) + '\s*'

    if ($Content -match $pattern) {
        $updated = [regex]::Replace($Content, $pattern, ([Environment]::NewLine + [Environment]::NewLine + $managedBlock + [Environment]::NewLine + [Environment]::NewLine))
        return $updated.TrimEnd() + [Environment]::NewLine
    }

    if ([string]::IsNullOrWhiteSpace($Content)) {
        return $managedBlock + [Environment]::NewLine
    }

    return $Content.TrimEnd() + [Environment]::NewLine + [Environment]::NewLine + $managedBlock + [Environment]::NewLine
}

function Get-BulletMetadataValue {
    param(
        [string[]]$Lines,
        [string]$Label
    )

    $pattern = '^\s*-\s*\*\*' + [regex]::Escape($Label) + '\*\*:\s*(.+?)\s*$'
    foreach ($line in $Lines) {
        if ($line -match $pattern) {
            return $Matches[1].Trim()
        }
    }

    return $null
}

function Get-TierForFailureCount {
    param([int]$Count)

    if ($Count -ge 2) {
        return 'deep'
    }

    if ($Count -ge 1) {
        return 'balanced'
    }

    return 'efficiency'
}

function Get-EscalationState {
    param([string]$StateContent)

    $defaultState = Get-DefaultEscalationState
    $blockContent = Get-ManagedBlockContent -Content $StateContent -BlockName 'escalation-state'
    if ([string]::IsNullOrWhiteSpace($blockContent)) {
        return $defaultState
    }

    $lines = @($blockContent -split "`r?`n")
    $status = Get-BulletMetadataValue -Lines $lines -Label 'Status'
    $artifact = Get-BulletMetadataValue -Lines $lines -Label 'Artifact'
    $gate = Get-BulletMetadataValue -Lines $lines -Label 'Gate'
    $failureCount = Get-BulletMetadataValue -Lines $lines -Label 'Failure Count'
    $currentTier = Get-BulletMetadataValue -Lines $lines -Label 'Current Tier'
    $currentOwner = Get-BulletMetadataValue -Lines $lines -Label 'Current Owner'
    $lockedOutAgents = Get-BulletMetadataValue -Lines $lines -Label 'Locked Out Agents'
    $lastEscalated = Get-BulletMetadataValue -Lines $lines -Label 'Last Escalated'
    $resolvedAt = Get-BulletMetadataValue -Lines $lines -Label 'Resolved At'
    $notes = Get-BulletMetadataValue -Lines $lines -Label 'Notes'

    $parsedFailureCount = 0
    if (-not [string]::IsNullOrWhiteSpace($failureCount)) {
        [void][int]::TryParse($failureCount.Trim(), [ref]$parsedFailureCount)
    }

    $lockedOut = @()
    if (-not (Test-IsNullish $lockedOutAgents)) {
        $lockedOut = @(
            $lockedOutAgents -split ',' |
                ForEach-Object { $_.Trim() } |
                Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
        )
    }

    return [pscustomobject]@{
        status            = if (Test-IsNullish $status) { $defaultState.status } else { $status.Trim().ToLowerInvariant() }
        artifact          = if (Test-IsNullish $artifact) { $null } else { $artifact.Trim() }
        gate              = if (Test-IsNullish $gate) { $null } else { $gate.Trim() }
        failure_count     = $parsedFailureCount
        current_tier      = if (Test-IsNullish $currentTier) { $defaultState.current_tier } else { $currentTier.Trim().ToLowerInvariant() }
        current_owner     = if (Test-IsNullish $currentOwner) { $null } else { $currentOwner.Trim() }
        locked_out_agents = @($lockedOut)
        last_escalated    = if (Test-IsNullish $lastEscalated) { $null } else { $lastEscalated.Trim() }
        resolved_at       = if (Test-IsNullish $resolvedAt) { $null } else { $resolvedAt.Trim() }
        notes             = if (Test-IsNullish $notes) { $null } else { $notes.Trim() }
    }
}

function Format-EscalationStateBlock {
    param([pscustomobject]$State)

    return @"
## Repair Escalation
 
- **Status**: $(if (Test-IsNullish $State.status) { 'inactive' } else { $State.status })
- **Artifact**: $(if (Test-IsNullish $State.artifact) { '(none)' } else { $State.artifact })
- **Gate**: $(if (Test-IsNullish $State.gate) { '(none)' } else { $State.gate })
- **Failure Count**: $([int]$State.failure_count)
- **Current Tier**: $(if (Test-IsNullish $State.current_tier) { 'efficiency' } else { $State.current_tier })
- **Current Owner**: $(if (Test-IsNullish $State.current_owner) { '(none)' } else { $State.current_owner })
- **Locked Out Agents**: $(if ($null -ne $State.locked_out_agents -and $State.locked_out_agents.Count -gt 0) { $State.locked_out_agents -join ', ' } else { '(none)' })
- **Last Escalated**: $(if (Test-IsNullish $State.last_escalated) { '(none)' } else { $State.last_escalated })
- **Resolved At**: $(if (Test-IsNullish $State.resolved_at) { '(none)' } else { $State.resolved_at })
- **Notes**: $(if (Test-IsNullish $State.notes) { '(none)' } else { $State.notes })
"@

}

$resolvedIterationDirectory = [System.IO.Path]::GetFullPath($IterationDirectory)
$statePath = Join-Path -Path $resolvedIterationDirectory -ChildPath 'state.md'
if (-not (Test-Path -LiteralPath $statePath -PathType Leaf)) {
    throw "Iteration state '$statePath' does not exist."
}

$projectRoot = Resolve-ProjectRoot -StartPath $resolvedIterationDirectory
$nextState = $null
$stateContent = $null
$currentState = $null
$timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')

if ($Mode -eq 'get') {
    $stateContent = Get-Content -LiteralPath $statePath -Raw -Encoding UTF8
    $currentState = Get-EscalationState -StateContent $stateContent
    $nextState = $currentState
}
else {
    $hasFailureCount = $PSBoundParameters.ContainsKey('FailureCount')
    $hasTier = $PSBoundParameters.ContainsKey('Tier')
    $hasNotes = $PSBoundParameters.ContainsKey('Notes')
    $updateEscalation = {
        param([string]$CurrentContent)

        $script:stateContent = $CurrentContent
        $script:currentState = Get-EscalationState -StateContent $CurrentContent

        switch ($Mode) {
            'activate' {
                if ([string]::IsNullOrWhiteSpace($Artifact)) {
                    throw 'Mode activate requires -Artifact.'
                }

                if ([string]::IsNullOrWhiteSpace($Gate)) {
                    throw 'Mode activate requires -Gate.'
                }

                if ([string]::IsNullOrWhiteSpace($Owner)) {
                    throw 'Mode activate requires -Owner.'
                }

                $sameEscalation = $script:currentState.status -eq 'active' -and $script:currentState.artifact -eq $Artifact -and $script:currentState.gate -eq $Gate
                $resolvedFailureCount = if ($hasFailureCount) {
                    $FailureCount
                }
                elseif ($sameEscalation) {
                    $script:currentState.failure_count + 1
                }
                else {
                    1
                }

                $resolvedTier = if ($hasTier) { $Tier } else { Get-TierForFailureCount -Count $resolvedFailureCount }
                $lockoutSet = New-Object System.Collections.Generic.HashSet[string] ([System.StringComparer]::OrdinalIgnoreCase)
                foreach ($agentName in @($script:currentState.locked_out_agents + $LockedOutAgents)) {
                    if (-not [string]::IsNullOrWhiteSpace($agentName)) {
                        $null = $lockoutSet.Add($agentName.Trim())
                    }
                }

                if ($sameEscalation -and -not [string]::IsNullOrWhiteSpace($script:currentState.current_owner)) {
                    $null = $lockoutSet.Add($script:currentState.current_owner.Trim())
                }

                if ($lockoutSet.Contains($Owner.Trim())) {
                    $null = $lockoutSet.Remove($Owner.Trim())
                }

                $noteValue = if ($hasNotes) {
                    $Notes
                }
                elseif ($sameEscalation) {
                    $script:currentState.notes
                }
                else {
                    $null
                }

                $script:nextState = [pscustomobject]@{
                    status            = 'active'
                    artifact          = $Artifact.Trim()
                    gate              = $Gate.Trim()
                    failure_count     = $resolvedFailureCount
                    current_tier      = $resolvedTier
                    current_owner     = $Owner.Trim()
                    locked_out_agents = @($lockoutSet | Sort-Object)
                    last_escalated    = $timestamp
                    resolved_at       = $null
                    notes             = if (Test-IsNullish $noteValue) { $null } else { $noteValue.Trim() }
                }
            }
            'resolve' {
                $script:nextState = [pscustomobject]@{
                    status            = 'inactive'
                    artifact          = $null
                    gate              = $null
                    failure_count     = 0
                    current_tier      = 'efficiency'
                    current_owner     = $null
                    locked_out_agents = @()
                    last_escalated    = $null
                    resolved_at       = $timestamp
                    notes             = if (Test-IsNullish $Notes) { 'Resolved after governance gate passed.' } else { $Notes.Trim() }
                }
            }
            'clear' {
                $script:nextState = Get-DefaultEscalationState
            }
        }

        $blockContent = Format-EscalationStateBlock -State $script:nextState
        return Set-ManagedBlock -Content $CurrentContent -BlockName 'escalation-state' -BlockContent $blockContent
    }

    if ($DryRun) {
        $stateContent = Get-Content -LiteralPath $statePath -Raw -Encoding UTF8
        $null = & $updateEscalation $stateContent
    }
    else {
        $null = Update-LockedFileContent -Path $statePath -Transform $updateEscalation
    }
}

if (-not $DryRun -and $Mode -ne 'get') {
    $entryTitle = switch ($Mode) {
        'activate' { 'Repair escalation activated' }
        'resolve' { 'Repair escalation resolved' }
        'clear' { 'Repair escalation cleared' }
        default { 'Repair escalation updated' }
    }

    $lockedOutDisplay = if ($nextState.locked_out_agents.Count -gt 0) { $nextState.locked_out_agents -join ', ' } else { '(none)' }
    $notesDisplay = if (Test-IsNullish $nextState.notes) { '(none)' } else { $nextState.notes }
    $relativeIterationDirectory = [System.IO.Path]::GetRelativePath($projectRoot, $resolvedIterationDirectory) -replace '/', '\'
    Add-StructuredDecisionsLedgerEntry -ProjectRoot $projectRoot -Title $entryTitle -Type 'escalation' -AffectedRequirement 'FR-027' -AffectedIteration $relativeIterationDirectory -NextAction 'none' -Rationale ("Repair escalation state changed to '{0}' for artifact '{1}'." -f $nextState.status, $(if (Test-IsNullish $nextState.artifact) { '(none)' } else { $nextState.artifact })) -DetailLines @(
        "- **Iteration**: $resolvedIterationDirectory"
        "- **Artifact**: $(if (Test-IsNullish $nextState.artifact) { '(none)' } else { $nextState.artifact })"
        "- **Gate**: $(if (Test-IsNullish $nextState.gate) { '(none)' } else { $nextState.gate })"
        "- **Status**: $($nextState.status)"
        "- **Owner**: $(if (Test-IsNullish $nextState.current_owner) { '(none)' } else { $nextState.current_owner })"
        "- **Tier**: $($nextState.current_tier)"
        "- **Failure Count**: $([int]$nextState.failure_count)"
        "- **Locked Out Agents**: $lockedOutDisplay"
        "- **Notes**: $notesDisplay"
    ) | Out-Null
}

if ($PassThru) {
    $nextState
    return
}

$nextState | ConvertTo-Json -Depth 5
exit 0