extensions/specrew-speckit/scripts/sync-squad-model-overrides.ps1
|
[CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$IterationDirectory, [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 Get-SquadConfigPath { param([string]$Root) return Join-Path $Root '.squad\config.json' } function Get-SquadConfig { param([string]$Root) $configPath = Get-SquadConfigPath -Root $Root if (-not (Test-Path -LiteralPath $configPath -PathType Leaf)) { return [ordered]@{ version = 1 } } $config = Get-Content -LiteralPath $configPath -Raw -Encoding UTF8 | ConvertFrom-Json -AsHashtable if ($null -eq $config) { return [ordered]@{ version = 1 } } return $config } function Convert-ToOrderedMap { param([AllowNull()]$Value) $result = [ordered]@{} if ($null -eq $Value) { return $result } if ($Value -is [System.Collections.IDictionary]) { foreach ($key in $Value.Keys) { $result[[string]$key] = $Value[$key] } return $result } foreach ($property in $Value.PSObject.Properties) { $result[$property.Name] = $property.Value } return $result } function Test-MapKey { param( [System.Collections.IDictionary]$Map, [string]$Key ) if ($null -eq $Map) { return $false } return $Map.Contains($Key) } function Get-ManagedRoutingMetadata { param([System.Collections.IDictionary]$Config) if (Test-MapKey -Map $Config -Key 'specrewManagedModelRouting') { return Convert-ToOrderedMap -Value $Config['specrewManagedModelRouting'] } return [ordered]@{} } function Get-BaselineOverrides { param( [System.Collections.IDictionary]$Config, [System.Collections.IDictionary]$ManagedRouting ) if (Test-MapKey -Map $ManagedRouting -Key 'baselineAgentModelOverrides') { return Convert-ToOrderedMap -Value $ManagedRouting['baselineAgentModelOverrides'] } if (Test-MapKey -Map $Config -Key 'agentModelOverrides') { return Convert-ToOrderedMap -Value $Config['agentModelOverrides'] } return [ordered]@{} } function Get-RoleAgentFamilies { param([System.Collections.IDictionary]$ManagedRouting) if (Test-MapKey -Map $ManagedRouting -Key 'roleAgentFamilies') { return Convert-ToOrderedMap -Value $ManagedRouting['roleAgentFamilies'] } return [ordered]@{} } function Get-ModelForEscalation { param( [string]$AgentFamily, [string]$Tier ) switch ($AgentFamily) { 'claude' { switch ($Tier) { 'deep' { return 'claude-opus-4.7' } 'balanced' { return 'claude-sonnet-4.5' } default { return 'claude-haiku-4.5' } } } 'codex' { switch ($Tier) { 'deep' { return 'gpt-5.5' } 'balanced' { return 'gpt-5.3-codex' } default { return 'gpt-5.2-codex' } } } default { switch ($Tier) { 'deep' { return 'gpt-5.4' } 'balanced' { return 'gpt-5.2' } default { return 'gpt-5-mini' } } } } } $resolvedIterationDirectory = [System.IO.Path]::GetFullPath($IterationDirectory) $projectRoot = Resolve-ProjectRoot -StartPath $resolvedIterationDirectory $manageEscalationPath = Join-Path $PSScriptRoot 'manage-escalation-state.ps1' if (-not (Test-Path -LiteralPath $manageEscalationPath -PathType Leaf)) { throw "Missing escalation helper '$manageEscalationPath'." } $escalation = & $manageEscalationPath -IterationDirectory $resolvedIterationDirectory -Mode get -PassThru $configPath = Get-SquadConfigPath -Root $projectRoot $appliedModel = $null $roleName = $null $effectiveOverrides = [ordered]@{} $syncTimestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') $null = Update-LockedFileContent -Path $configPath -Transform { param([string]$CurrentContent) $config = if ([string]::IsNullOrWhiteSpace($CurrentContent)) { [ordered]@{ version = 1 } } else { $parsed = $CurrentContent | ConvertFrom-Json -AsHashtable if ($null -eq $parsed) { [ordered]@{ version = 1 } } else { $parsed } } if (-not (Test-MapKey -Map $config -Key 'version')) { $config['version'] = 1 } $managedRouting = Get-ManagedRoutingMetadata -Config $config $baselineOverrides = Get-BaselineOverrides -Config $config -ManagedRouting $managedRouting $roleAgentFamilies = Get-RoleAgentFamilies -ManagedRouting $managedRouting $script:effectiveOverrides = [ordered]@{} foreach ($key in $baselineOverrides.Keys) { $script:effectiveOverrides[$key] = $baselineOverrides[$key] } $script:appliedModel = $null $script:roleName = $null if ($escalation.status -eq 'active' -and -not [string]::IsNullOrWhiteSpace($escalation.current_owner)) { $script:roleName = $escalation.current_owner.Trim() $agentFamily = if (Test-MapKey -Map $roleAgentFamilies -Key $script:roleName) { [string]$roleAgentFamilies[$script:roleName] } else { 'copilot' } $script:appliedModel = Get-ModelForEscalation -AgentFamily $agentFamily -Tier $escalation.current_tier $script:effectiveOverrides[$script:roleName] = $script:appliedModel } if ($script:effectiveOverrides.Count -gt 0) { $config['agentModelOverrides'] = $script:effectiveOverrides } # Spec 008 Extension: Sync reviewer-regression state # This extension adds reviewerRegressionState alongside activeEscalation without modifying FR-027 behavior if ($null -eq $config['reviewerRegressionState']) { $config['reviewerRegressionState'] = [ordered]@{ status = 'inactive' feature = $null currentReviewerClass = $null currentReviewerOwner = $null lockoutChainLength = 0 capActive = $false updatedAt = $null } } # Project active reviewer-regression state from the current iteration if present $stateFilePath = Join-Path $resolvedIterationDirectory 'state.md' if (Test-Path -LiteralPath $stateFilePath -PathType Leaf) { $stateLines = @(Get-Content -LiteralPath $stateFilePath -Encoding UTF8) $inRegressionBlock = $false $regressionStatus = 'inactive' $regressionFeature = $null $currentReviewerClass = $null $currentReviewerOwner = $null $lockoutChainLength = 0 $capActive = $false foreach ($line in $stateLines) { if ($line -match '<!-- >>> specrew-managed reviewer-regression-state >>> -->') { $inRegressionBlock = $true continue } if ($line -match '<!-- <<< specrew-managed reviewer-regression-state <<< -->') { $inRegressionBlock = $false continue } if ($inRegressionBlock) { if ($line -match '^\s*-\s+\*\*Status\*\*:\s*(.+?)\s*$') { $regressionStatus = $Matches[1].Trim() } elseif ($line -match '^\s*-\s+\*\*Feature\*\*:\s*(.+?)\s*$') { $featureValue = $Matches[1].Trim() if ($featureValue -ne '(none)') { $regressionFeature = $featureValue } } elseif ($line -match '^\s*-\s+\*\*Current Reviewer Class\*\*:\s*(.+?)\s*$') { $classValue = $Matches[1].Trim() if ($classValue -ne '(none)') { $currentReviewerClass = $classValue } } elseif ($line -match '^\s*-\s+\*\*Current Reviewer Owner\*\*:\s*(.+?)\s*$') { $ownerValue = $Matches[1].Trim() if ($ownerValue -ne '(none)') { $currentReviewerOwner = $ownerValue } } elseif ($line -match '^\s*-\s+\*\*Lockout Chain Length\*\*:\s*(\d+)') { $lockoutChainLength = [int]$Matches[1] } elseif ($line -match '^\s*-\s+\*\*Cap Active\*\*:\s*(true|false)') { $capActive = $Matches[1] -eq 'true' } } } # Update config with parsed reviewer-regression state $config['reviewerRegressionState']['status'] = $regressionStatus $config['reviewerRegressionState']['feature'] = $regressionFeature $config['reviewerRegressionState']['currentReviewerClass'] = $currentReviewerClass $config['reviewerRegressionState']['currentReviewerOwner'] = $currentReviewerOwner $config['reviewerRegressionState']['lockoutChainLength'] = $lockoutChainLength $config['reviewerRegressionState']['capActive'] = $capActive $config['reviewerRegressionState']['updatedAt'] = $syncTimestamp } elseif (Test-MapKey -Map $config -Key 'agentModelOverrides') { $config.Remove('agentModelOverrides') } $managedRouting['baselineAgentModelOverrides'] = $baselineOverrides $managedRouting['roleAgentFamilies'] = $roleAgentFamilies $managedRouting['activeEscalation'] = [ordered]@{ status = $escalation.status role = $script:roleName tier = $escalation.current_tier sourceIteration = $resolvedIterationDirectory sourceArtifact = $escalation.artifact sourceGate = $escalation.gate updatedAt = $syncTimestamp } $config['specrewManagedModelRouting'] = $managedRouting return (($config | ConvertTo-Json -Depth 10) + [Environment]::NewLine) } Add-DecisionsLedgerEntry -ProjectRoot $projectRoot -Title 'Repair escalation routing sync' -Lines @( "- **Iteration**: $resolvedIterationDirectory" "- **Escalation Status**: $($escalation.status)" "- **Escalation Artifact**: $(if ([string]::IsNullOrWhiteSpace($escalation.artifact)) { '(none)' } else { $escalation.artifact })" "- **Escalation Gate**: $(if ([string]::IsNullOrWhiteSpace($escalation.gate)) { '(none)' } else { $escalation.gate })" "- **Role**: $(if ([string]::IsNullOrWhiteSpace($roleName)) { '(none)' } else { $roleName })" "- **Tier**: $($escalation.current_tier)" "- **Applied Model**: $(if ([string]::IsNullOrWhiteSpace($appliedModel)) { '(none)' } else { $appliedModel })" "- **Override Count**: $($effectiveOverrides.Count)" ) | Out-Null $result = [pscustomobject]@{ project_root = $projectRoot iteration_directory = $resolvedIterationDirectory escalation_status = $escalation.status escalation_role = $roleName escalation_tier = $escalation.current_tier applied_model = $appliedModel agent_model_overrides = $effectiveOverrides } if ($PassThru) { $result return } $result | ConvertTo-Json -Depth 10 exit 0 |