scripts/internal/coordinator-resume.ps1
|
Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $sharedGovernancePath = Join-Path (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)) 'extensions\specrew-speckit\scripts\shared-governance.ps1' if (-not (Test-Path -LiteralPath $sharedGovernancePath -PathType Leaf)) { throw "Missing shared governance helper '$sharedGovernancePath'." } . $sharedGovernancePath $taskProgressHelperPath = Join-Path $PSScriptRoot 'task-progress.ps1' if (-not (Test-Path -LiteralPath $taskProgressHelperPath -PathType Leaf)) { throw "Missing task-progress helper '$taskProgressHelperPath'." } . $taskProgressHelperPath $worktreeHelperPath = Join-Path $PSScriptRoot 'worktree-awareness.ps1' if (-not (Test-Path -LiteralPath $worktreeHelperPath -PathType Leaf)) { throw "Missing worktree-awareness helper '$worktreeHelperPath'." } . $worktreeHelperPath $boundaryStateHelperPath = Join-Path $PSScriptRoot 'sync-boundary-state.ps1' if (-not (Test-Path -LiteralPath $boundaryStateHelperPath -PathType Leaf)) { throw "Missing boundary-state helper '$boundaryStateHelperPath'." } . $boundaryStateHelperPath function Get-ValidatorSummaryPath { param([Parameter(Mandatory = $true)][string]$ProjectRoot) return Get-SpecrewValidatorSummaryPath -ProjectRoot $ProjectRoot } function Get-ValidatorWarningSummary { param([Parameter(Mandatory = $true)][string]$ProjectRoot) $summaryPath = Get-ValidatorSummaryPath -ProjectRoot $ProjectRoot if (-not (Test-Path -LiteralPath $summaryPath -PathType Leaf)) { return $null } try { # F-023: Use -AsHashtable for StrictMode compatibility; hashtable indexer tolerates missing fields $summary = Get-Content -LiteralPath $summaryPath -Raw -Encoding UTF8 | ConvertFrom-Json -AsHashtable -Depth 6 # F-023: Legacy schema handling - missing 'schema' field implies v0 $schema = Get-SpecrewStateSchemaVersion -State $summary -Path $summaryPath # v0/v1 behavior: warnings field structure is required in both schemas # v1+ behavior: same as v0 for this summary (no behavioral divergence yet) return [pscustomobject]@{ total = [int]$summary['warnings']['total'] soft = [int]$summary['warnings']['soft'] medium = [int]$summary['warnings']['medium'] hard = [int]$summary['warnings']['hard'] command = [string]$summary['command'] recorded_at = [string]$summary['recorded_at'] } } catch { if (Test-IsUnsupportedSpecrewSchemaError -ErrorRecord $_) { throw } return $null } } function Resolve-ResumeIterationNumber { param( [AllowNull()][string]$ResolvedFeaturePath, [AllowNull()][pscustomobject]$SessionState ) if ($null -ne $SessionState -and -not [string]::IsNullOrWhiteSpace([string]$SessionState.iteration_number)) { return [string]$SessionState.iteration_number } if ([string]::IsNullOrWhiteSpace($ResolvedFeaturePath)) { return $null } $iterationsRoot = Join-Path $ResolvedFeaturePath 'iterations' if (-not (Test-Path -LiteralPath $iterationsRoot -PathType Container)) { return $null } $latestIteration = Get-ChildItem -LiteralPath $iterationsRoot -Directory | Where-Object { Test-Path -LiteralPath (Join-Path $_.FullName 'plan.md') -PathType Leaf } | Sort-Object Name -Descending | Select-Object -First 1 if ($null -eq $latestIteration) { return $null } return [string]$latestIteration.Name } function Get-CoordinatorResumeSnapshot { param( [Parameter(Mandatory = $true)][string]$ProjectRoot, [AllowNull()][string]$ResolvedFeaturePath, [AllowNull()][pscustomobject]$SessionState ) $resolvedProjectRoot = Resolve-ProjectPath -Path $ProjectRoot $effectiveFeaturePath = if (-not [string]::IsNullOrWhiteSpace($ResolvedFeaturePath)) { $ResolvedFeaturePath } elseif ($null -ne $SessionState -and -not [string]::IsNullOrWhiteSpace([string]$SessionState.feature_path)) { [string]$SessionState.feature_path } else { $null } $featureRef = if ($null -ne $SessionState -and -not [string]::IsNullOrWhiteSpace([string]$SessionState.feature_ref)) { [string]$SessionState.feature_ref } elseif (-not [string]::IsNullOrWhiteSpace($effectiveFeaturePath)) { Split-Path -Leaf $effectiveFeaturePath } else { $null } $iterationNumber = Resolve-ResumeIterationNumber -ResolvedFeaturePath $effectiveFeaturePath -SessionState $SessionState $taskSummary = if (-not [string]::IsNullOrWhiteSpace($featureRef) -and -not [string]::IsNullOrWhiteSpace($iterationNumber)) { Get-TaskProgressSummary -ProjectRoot $resolvedProjectRoot -FeatureRef $featureRef -IterationNumber $iterationNumber -ResolvedFeaturePath $effectiveFeaturePath } else { $null } $latestBoundary = Get-LatestSpecrewBoundarySyncState -ProjectRoot $resolvedProjectRoot $validatorSummary = Get-ValidatorWarningSummary -ProjectRoot $resolvedProjectRoot $suggestedActions = New-Object System.Collections.Generic.List[string] if ($null -ne $taskSummary -and $taskSummary.InProgress.Count -gt 0) { $task = $taskSummary.InProgress[0] $suggestedActions.Add(("Resume {0} — {1}" -f $task.id, $task.title)) | Out-Null } elseif ($null -ne $taskSummary -and $taskSummary.Pending.Count -gt 0) { $task = $taskSummary.Pending[0] $suggestedActions.Add(("Start {0} — {1}" -f $task.id, $task.title)) | Out-Null } if ($null -ne $validatorSummary -and $validatorSummary.total -gt 0) { $validatorCommand = if (-not [string]::IsNullOrWhiteSpace($validatorSummary.command)) { $validatorSummary.command } elseif (-not [string]::IsNullOrWhiteSpace($featureRef) -and -not [string]::IsNullOrWhiteSpace($iterationNumber)) { 'pwsh -NoProfile -ExecutionPolicy Bypass -File .\extensions\specrew-speckit\scripts\validate-governance.ps1 -ProjectPath . -IterationPath .\specs\' + $featureRef + '\iterations\' + $iterationNumber } else { 'pwsh -NoProfile -ExecutionPolicy Bypass -File .\extensions\specrew-speckit\scripts\validate-governance.ps1 -ProjectPath .' } $suggestedActions.Add(("Review validator warnings with: {0}" -f $validatorCommand)) | Out-Null } return [pscustomobject]@{ feature_ref = $featureRef feature_path = $effectiveFeaturePath worktree_path = $resolvedProjectRoot iteration_number = $iterationNumber current_boundary = if ($null -ne $SessionState) { [string]$SessionState.boundary_type } else { $null } current_task = if ($null -ne $SessionState) { [string]$SessionState.task_id } else { $null } last_boundary_commit = if ($null -ne $latestBoundary) { [string]$latestBoundary.auth_commit_hash } else { $null } last_boundary_at = if ($null -ne $latestBoundary) { [string]$latestBoundary.recorded_at } else { $null } task_summary = $taskSummary validator_summary = $validatorSummary suggested_actions = $suggestedActions.ToArray() } } function Get-CoordinatorResumePromptBlock { param( [Parameter(Mandatory = $true)][string]$ProjectRoot, [AllowNull()][string]$ResolvedFeaturePath, [AllowNull()][pscustomobject]$SessionState ) $snapshot = Get-CoordinatorResumeSnapshot -ProjectRoot $ProjectRoot -ResolvedFeaturePath $ResolvedFeaturePath -SessionState $SessionState if ([string]::IsNullOrWhiteSpace($snapshot.feature_ref)) { return $null } $taskSummary = $snapshot.task_summary $taskStatusLine = if ($null -eq $taskSummary) { 'Task progress: (not available)' } else { 'Task progress: {0} complete, {1} in-progress, {2} pending, {3} blocked' -f $taskSummary.Complete.Count, $taskSummary.InProgress.Count, $taskSummary.Pending.Count, $taskSummary.Blocked.Count } $taskDetails = @() if ($null -ne $taskSummary -and $taskSummary.Complete.Count -gt 0) { $taskDetails += '- Complete: ' + (($taskSummary.Complete | ForEach-Object { $_.id }) -join ', ') } if ($null -ne $taskSummary -and $taskSummary.InProgress.Count -gt 0) { $taskDetails += '- In progress: ' + (($taskSummary.InProgress | ForEach-Object { '{0} ({1})' -f $_.id, $_.title }) -join '; ') } if ($null -ne $taskSummary -and $taskSummary.Pending.Count -gt 0) { $taskDetails += '- Pending: ' + (($taskSummary.Pending | Select-Object -First 3 | ForEach-Object { $_.id }) -join ', ') } if ($null -ne $taskSummary -and $taskSummary.Blocked.Count -gt 0) { $taskDetails += '- Blocked: ' + (($taskSummary.Blocked | ForEach-Object { '{0} ({1})' -f $_.id, $_.blocked_reason }) -join '; ') } $validatorLine = if ($null -eq $snapshot.validator_summary -or $snapshot.validator_summary.total -le 0) { 'Validator state: no recorded warnings' } else { 'Validator state: {0} warnings: {1} soft, {2} medium, {3} hard' -f $snapshot.validator_summary.total, $snapshot.validator_summary.soft, $snapshot.validator_summary.medium, $snapshot.validator_summary.hard } $lastCompletedTaskLine = if ($null -ne $taskSummary -and $null -ne $taskSummary.LatestCompleted) { '- Last completed task: {0} at {1}' -f $taskSummary.LatestCompleted.id, $taskSummary.LatestCompleted.timestamp } else { '- Last completed task: (none)' } $suggestedActionsBlock = if ($snapshot.suggested_actions.Count -gt 0) { ($snapshot.suggested_actions | ForEach-Object { '- ' + $_ }) -join [Environment]::NewLine } else { '- Continue from the current feature boundary' } $detailBlock = if ($taskDetails.Count -gt 0) { ($taskDetails -join [Environment]::NewLine) + [Environment]::NewLine } else { '' } return @" ## Welcome Back Snapshot - Active feature: $($snapshot.feature_ref) - Feature path: $(if ($snapshot.feature_path) { $snapshot.feature_path } else { '(none)' }) - Worktree: $($snapshot.worktree_path) - Current boundary: $(if ($snapshot.current_boundary) { $snapshot.current_boundary } else { '(none)' }) - Current task: $(if ($snapshot.current_task) { $snapshot.current_task } else { '(none)' }) $lastCompletedTaskLine - Last completed boundary: $(if ($snapshot.last_boundary_commit) { "$($snapshot.last_boundary_commit) at $($snapshot.last_boundary_at)" } else { '(none)' }) - $taskStatusLine $detailBlock- $validatorLine ### Suggested Next Actions $suggestedActionsBlock "@ } function Get-CoordinatorRecoveryPromptBlock { param( [AllowNull()][pscustomobject]$RecoverySession ) if ($null -eq $RecoverySession) { return $null } $reasonLines = if (@($RecoverySession.stale_reasons).Count -gt 0) { (@($RecoverySession.stale_reasons) | ForEach-Object { '- ' + [string]$_ }) -join [Environment]::NewLine } else { '- Recovery was requested explicitly.' } $choiceLines = if (@($RecoverySession.choice_set).Count -gt 0) { (@($RecoverySession.choice_set) | ForEach-Object { '- ' + [string]$_ }) -join [Environment]::NewLine } else { '- Recovery is active without an interactive choice requirement.' } return @" ## Recovery Mode - Entry mode: $($RecoverySession.entry_mode) - Selected choice: $(if ($RecoverySession.selected_choice) { $RecoverySession.selected_choice } else { '(none)' }) - Bypass stale-state gate: $($RecoverySession.bypass_gate) - Approval behavior changed: $($RecoverySession.approval_mode_changed) - Next action: $($RecoverySession.next_action_message) ### Recovery Reasons $reasonLines ### Available Recovery Choices $choiceLines "@ } |