extensions/specrew-speckit/scripts/refocus.ps1
|
# Specrew Refocus Engine (Feature 171, FR-001/FR-003/FR-004/FR-005/FR-012/FR-017). # The SINGLE payload engine for every refocus surface: the /specrew-refocus slash # command, the hook dispatcher's providers, the boundary-sync wrapper emission, and # humans at a prompt. Pure with respect to payload production: same inputs -> same # payload; the engine NEVER dedupes (a human asking always gets payload) and never # writes state on payload paths. Operator commands (--status / --reset-breaker) are # the contracted exceptions that read/clear runtime state. # # Contract (C1): stdout line 1 is the banner # [specrew-refocus] trigger=<t> scope=<s> sources=<n> tokens~<est> # Warnings go to stderr as: [specrew-refocus] WARN <CODE> <message> # Reason codes (FR-012): EVENT_PARSE, CATALOG_SCHEMA, SOURCE_MISSING, SOURCE_CONFINED, # STATE_UNAVAILABLE, BUDGET_EXCEEDED, BREAKER_TRIPPED, PROVIDER_FAILED # Exit codes: 0 on success AND on every fail-open path; 2 only for human arg errors # (the dispatcher never passes bad args). [CmdletBinding()] param( [Parameter(ValueFromRemainingArguments = $true)] [string[]]$Arguments ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $script:Banner = '[specrew-refocus]' $script:CatalogSchemaVersion = '1' function Write-RefocusWarn { param( [Parameter(Mandatory = $true)][string]$Code, [Parameter(Mandatory = $true)][string]$Message ) # Error envelope (C1): one line per warning, stderr, machine-greppable code. [Console]::Error.WriteLine(("{0} WARN {1} {2}" -f $script:Banner, $Code, $Message)) } function Get-RefocusProjectRoot { # Walk up from the current location to the nearest .specrew project root. The # engine is invoked with the project as CWD by every surface (skill, wrapper, # dispatcher); walking up tolerates subdirectory invocations. $candidate = (Get-Location).Path while (-not [string]::IsNullOrWhiteSpace($candidate)) { if (Test-Path -LiteralPath (Join-Path $candidate '.specrew') -PathType Container) { return $candidate } $parent = Split-Path -Parent $candidate if ($parent -eq $candidate) { break } $candidate = $parent } return $null } function Test-RefocusConfinedPath { param([Parameter(Mandatory = $true)][string]$RelativePath) # FR-004: content sources are repo-relative ONLY. Refuse absolute paths, drive # roots, and any '..' traversal segment. if ([System.IO.Path]::IsPathRooted($RelativePath)) { return $false } $segments = $RelativePath -split '[\\/]' foreach ($segment in $segments) { if ($segment -eq '..') { return $false } } return $true } function Get-RefocusCatalog { param([Parameter(Mandatory = $true)][string]$ProjectRoot) # Deployed copy first (downstream projects), repo canonical second (self-host). # JSON, not YAML: Specrew deliberately avoids the powershell-yaml dependency # (see scripts/internal/yaml-list.ps1 + host-history.ps1 precedent); recorded # as reconciled drift against FR-003's incidental .yml extension. $candidates = @( (Join-Path $ProjectRoot '.specify/extensions/specrew-speckit/refocus-scopes.json'), (Join-Path $ProjectRoot 'extensions/specrew-speckit/refocus-scopes.json') ) foreach ($path in $candidates) { if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { continue } try { $catalog = Get-Content -LiteralPath $path -Raw -Encoding UTF8 | ConvertFrom-Json } catch { Write-RefocusWarn -Code 'CATALOG_SCHEMA' -Message ("catalog unreadable at {0}: {1}" -f $path, $_.Exception.Message) return $null } $declared = if ($catalog.PSObject.Properties['schema_version']) { [string]$catalog.schema_version } else { '' } if ($declared -ne $script:CatalogSchemaVersion) { Write-RefocusWarn -Code 'CATALOG_SCHEMA' -Message ("catalog schema_version '{0}' does not match engine '{1}' (additive evolution contract); failing open" -f $declared, $script:CatalogSchemaVersion) return $null } return $catalog } Write-RefocusWarn -Code 'SOURCE_MISSING' -Message 'refocus-scopes.json not found (deployed or canonical); run specrew update' return $null } function Get-RefocusDigestRoot { param([Parameter(Mandatory = $true)][string]$ProjectRoot) $candidates = @( (Join-Path $ProjectRoot '.specify/extensions/specrew-speckit/refocus'), (Join-Path $ProjectRoot 'extensions/specrew-speckit/refocus') ) foreach ($path in $candidates) { if (Test-Path -LiteralPath $path -PathType Container) { return $path } } return $null } function Read-RefocusDigest { param( [Parameter(Mandatory = $true)][string]$ProjectRoot, [Parameter(Mandatory = $true)][string]$DigestRelativePath ) # Returns @{ Body; SourceCount } or $null (with WARN already emitted). if (-not (Test-RefocusConfinedPath -RelativePath $DigestRelativePath)) { Write-RefocusWarn -Code 'SOURCE_CONFINED' -Message ("digest path '{0}' escapes the repository; refused" -f $DigestRelativePath) return $null } $digestRoot = Get-RefocusDigestRoot -ProjectRoot $ProjectRoot $resolved = $null if ($null -ne $digestRoot) { $candidate = Join-Path $digestRoot (Split-Path -Leaf $DigestRelativePath) if (Test-Path -LiteralPath $candidate -PathType Leaf) { $resolved = $candidate } } if ($null -eq $resolved) { # Tolerate catalog entries that carry fuller relative paths. $candidate = Join-Path $ProjectRoot $DigestRelativePath if (Test-Path -LiteralPath $candidate -PathType Leaf) { $resolved = $candidate } } if ($null -eq $resolved) { Write-RefocusWarn -Code 'SOURCE_MISSING' -Message ("digest '{0}' not found; run specrew update" -f $DigestRelativePath) return $null } $raw = Get-Content -LiteralPath $resolved -Raw -Encoding UTF8 # Digests cannot bake absolute URLs (they deploy to arbitrary projects); the # {{project_root}} placeholder resolves to the live root as a file:/// URL. $rootUrl = 'file:///' + ($ProjectRoot -replace '\\', '/') $raw = $raw.Replace('{{project_root}}', $rootUrl) $body = $raw $sourceCount = 0 # Frontmatter contract (C5): { scope, sources[], reviewed_at }. Strip it from the # injected body; count sources for the banner. $frontmatterMatch = [regex]::Match($raw, '(?s)^---\r?\n(?<fm>.*?)\r?\n---\r?\n') if ($frontmatterMatch.Success) { $body = $raw.Substring($frontmatterMatch.Length) $sourceCount = ([regex]::Matches($frontmatterMatch.Groups['fm'].Value, '(?m)^\s*-\s+\S')).Count } return @{ Body = $body.Trim(); SourceCount = $sourceCount } } function Get-RefocusStartContext { param([Parameter(Mandatory = $true)][string]$ProjectRoot) $path = Join-Path $ProjectRoot '.specrew/start-context.json' if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { return $null } try { return Get-Content -LiteralPath $path -Raw -Encoding UTF8 | ConvertFrom-Json } catch { return $null } } function Get-RefocusCurrentBoundary { param($StartContext) if ($null -eq $StartContext) { return $null } if ($StartContext.PSObject.Properties['session_state'] -and $null -ne $StartContext.session_state) { $boundary = [string]$StartContext.session_state.boundary_type if (-not [string]::IsNullOrWhiteSpace($boundary)) { return $boundary } } if ($StartContext.PSObject.Properties['boundary_enforcement'] -and $null -ne $StartContext.boundary_enforcement) { $boundary = [string]$StartContext.boundary_enforcement.last_authorized_boundary if (-not [string]::IsNullOrWhiteSpace($boundary)) { return $boundary } } return $null } function Get-RefocusBoundarySuccessor { param([AllowNull()][string]$Boundary) # Canonical stage order: after syncing boundary X, the work that follows is # the successor stage — B3 injects the INCOMING stage's discipline. $order = @('specify', 'clarify', 'plan', 'tasks', 'before-implement', 'implement', 'review-signoff', 'retro', 'iteration-closeout', 'feature-closeout') if ([string]::IsNullOrWhiteSpace($Boundary)) { return $null } $index = $order.IndexOf($Boundary.ToLowerInvariant()) if ($index -lt 0 -or $index -ge ($order.Count - 1)) { return $null } return $order[$index + 1] } function Get-RefocusTokenEstimate { param([AllowEmptyString()][string]$Text) if ([string]::IsNullOrEmpty($Text)) { return 0 } return [int][math]::Ceiling($Text.Length / 4.0) } function Get-RefocusRuntimeStateFiles { param([Parameter(Mandatory = $true)][string]$ProjectRoot) $runtimeDir = Join-Path $ProjectRoot '.specrew/runtime' if (-not (Test-Path -LiteralPath $runtimeDir -PathType Container)) { return @() } return @(Get-ChildItem -LiteralPath $runtimeDir -Filter 'refocus-state-*.json' -File -ErrorAction SilentlyContinue) } function Format-RefocusPayload { param( [Parameter(Mandatory = $true)][string]$Trigger, [Parameter(Mandatory = $true)][string]$Scope, [Parameter(Mandatory = $true)][AllowEmptyCollection()][object[]]$Parts, [Parameter(Mandatory = $true)][int]$SourceCount, [AllowNull()][nullable[int]]$BudgetCap ) # Assembles banner + body under the budget cap (FR-005): clip whole trailing # parts first, then lines, and always say so. $body = (@($Parts) -join "`n`n").Trim() $estimate = Get-RefocusTokenEstimate -Text $body if ($null -ne $BudgetCap -and $estimate -gt $BudgetCap) { $maxChars = [int]($BudgetCap * 4) $lines = $body -split "`n" $kept = New-Object System.Collections.Generic.List[string] $running = 0 foreach ($line in $lines) { $running += $line.Length + 1 if ($running -gt $maxChars) { break } $kept.Add($line) | Out-Null } $body = ($kept -join "`n").TrimEnd() $body += "`n`n> [specrew-refocus] payload clipped to the catalog budget cap (~{0} tokens); full content at the file:/// pointers above." -f $BudgetCap Write-RefocusWarn -Code 'BUDGET_EXCEEDED' -Message ("payload (~{0} tokens) clipped to cap {1} for scope '{2}'" -f $estimate, $BudgetCap, $Scope) $estimate = Get-RefocusTokenEstimate -Text $body } $banner = "{0} trigger={1} scope={2} sources={3} tokens~{4}" -f $script:Banner, $Trigger, $Scope, $SourceCount, $estimate return $banner + "`n`n" + $body } function Get-RefocusFallbackPointerSet { param([Parameter(Mandatory = $true)][string]$ProjectRoot) # Minimal always-works payload when the catalog or digests are unavailable: # point at the canonical corpus instead of going silent (fail-open). $rootUrl = 'file:///' + ($ProjectRoot -replace '\\', '/') return @( '## Specrew refocus (fallback pointer set)', '', ("- Constitution: {0}/.specrew/constitution.md" -f $rootUrl), ("- Coordinator governance: {0}/.specify/extensions/specrew-speckit/squad-templates/coordinator/specrew-governance.md" -f $rootUrl), ("- Methodology: {0}/docs/methodology/" -f $rootUrl), ("- Lifecycle prompt: {0}/.specrew/last-start-prompt.md" -f $rootUrl), '', 'Run `specrew update` to restore the refocus digest catalog.' ) -join "`n" } function Invoke-RefocusScopePayload { param( [Parameter(Mandatory = $true)][string]$ProjectRoot, [Parameter(Mandatory = $true)][string]$Trigger, [Parameter(Mandatory = $true)][string[]]$ScopeIds ) # ScopeIds are catalog scope keys (e.g. 'general', 'boundary.review-signoff'). $catalog = Get-RefocusCatalog -ProjectRoot $ProjectRoot $parts = New-Object System.Collections.Generic.List[string] $sources = 0 $cap = $null if ($null -eq $catalog) { $parts.Add((Get-RefocusFallbackPointerSet -ProjectRoot $ProjectRoot)) | Out-Null return Format-RefocusPayload -Trigger $Trigger -Scope ($ScopeIds -join '+') -Parts $parts.ToArray() -SourceCount 0 -BudgetCap $null } if ($catalog.PSObject.Properties['budgets'] -and $null -ne $catalog.budgets -and $catalog.budgets.PSObject.Properties[$Trigger]) { $cap = [int]$catalog.budgets.$Trigger } foreach ($scopeId in $ScopeIds) { if (-not ($catalog.PSObject.Properties['scopes'] -and $null -ne $catalog.scopes -and $catalog.scopes.PSObject.Properties[$scopeId])) { Write-RefocusWarn -Code 'SOURCE_MISSING' -Message ("scope '{0}' is not in the catalog; substituting the fallback pointer set" -f $scopeId) $parts.Add((Get-RefocusFallbackPointerSet -ProjectRoot $ProjectRoot)) | Out-Null continue } foreach ($digestPath in @($catalog.scopes.$scopeId)) { $digest = Read-RefocusDigest -ProjectRoot $ProjectRoot -DigestRelativePath ([string]$digestPath) if ($null -ne $digest) { $parts.Add($digest.Body) | Out-Null $sources += [int]$digest.SourceCount } } } # FR-023 / SC-016 (iter-4): surface the SELECTED work_kind's lifecycle CONTRACT at session-start / # refocus so the crew is pointed to its lifecycle BEFORE work begins (the DF-009 intake gap — the # validator runs too late). Guarded (no-op when no work_kind is declared) + fail-open (refocus must # never break). The resolver lives in the work-kind extension (same deployed scripts dir, or the # dev-tree extension path). try { $wkCommon = @( (Join-Path $PSScriptRoot 'work-kind-common.ps1'), (Join-Path $ProjectRoot 'extensions/specrew-speckit/scripts/work-kind-common.ps1'), (Join-Path $ProjectRoot '.specify/extensions/specrew-speckit/scripts/work-kind-common.ps1') ) | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1 if ($wkCommon) { . $wkCommon $lifecycleLine = Get-SpecrewWorkKindLifecycleSurface -ProjectRoot $ProjectRoot if (-not [string]::IsNullOrWhiteSpace($lifecycleLine)) { $parts.Add(("## Work-kind lifecycle (this work item)`n`n" + $lifecycleLine)) | Out-Null $sources += 1 } } } catch { Write-RefocusWarn -Code 'SOURCE_MISSING' -Message ("work-kind lifecycle surface unavailable: {0}" -f $_.Exception.Message) } if ($parts.Count -eq 0) { $parts.Add((Get-RefocusFallbackPointerSet -ProjectRoot $ProjectRoot)) | Out-Null } return Format-RefocusPayload -Trigger $Trigger -Scope ($ScopeIds -join '+') -Parts $parts.ToArray() -SourceCount $sources -BudgetCap $cap } function Invoke-RefocusRoleScope { param( [Parameter(Mandatory = $true)][string]$ProjectRoot, [Parameter(Mandatory = $true)][string]$RoleName ) # FR-001: --role loads the role charter directly (charters are already compact). $candidates = @( (Join-Path $ProjectRoot (".specrew/team/agents/{0}.md" -f $RoleName)), (Join-Path $ProjectRoot (".squad/agents/{0}/charter.md" -f $RoleName)) ) foreach ($path in $candidates) { if (Test-Path -LiteralPath $path -PathType Leaf) { $body = (Get-Content -LiteralPath $path -Raw -Encoding UTF8).Trim() return Format-RefocusPayload -Trigger 'manual' -Scope ("role.{0}" -f $RoleName) -Parts @($body) -SourceCount 1 -BudgetCap $null } } Write-RefocusWarn -Code 'SOURCE_MISSING' -Message ("role charter for '{0}' not found under .specrew/team/agents or .squad/agents" -f $RoleName) return Format-RefocusPayload -Trigger 'manual' -Scope ("role.{0}" -f $RoleName) -Parts @((Get-RefocusFallbackPointerSet -ProjectRoot $ProjectRoot)) -SourceCount 0 -BudgetCap $null } function Invoke-RefocusCompactInstructions { param([Parameter(Mandatory = $true)][string]$ProjectRoot) # FR-017: paste-ready /compact preserve-list from live lifecycle state. $ctx = Get-RefocusStartContext -ProjectRoot $ProjectRoot if ($null -eq $ctx) { Write-RefocusWarn -Code 'STATE_UNAVAILABLE' -Message 'start-context.json unreadable; emitting a generic preserve-list' return '/compact preserve: the active Specrew feature, current lifecycle boundary, active role, pending verdicts, and binding constraints; artifacts live under specs/<feature>/' } $feature = '' $boundary = '' if ($ctx.PSObject.Properties['session_state'] -and $null -ne $ctx.session_state) { $feature = [string]$ctx.session_state.feature_ref $boundary = [string]$ctx.session_state.boundary_type } if ([string]::IsNullOrWhiteSpace($feature)) { $feature = '<active feature>' } if ([string]::IsNullOrWhiteSpace($boundary)) { $boundary = '<current boundary>' } return ('/compact preserve: feature {0} at the {1} boundary, the active role and its charter rules, all pending human verdicts and binding constraints, boundary-commit discipline, and that artifacts/evidence live under specs/{0}/ (re-read them rather than trusting summary memory)' -f $feature, $boundary) } function Invoke-RefocusStatus { param([Parameter(Mandatory = $true)][string]$ProjectRoot) $lines = New-Object System.Collections.Generic.List[string] $lines.Add(("{0} status" -f $script:Banner)) | Out-Null $envDisabled = -not [string]::IsNullOrWhiteSpace($env:SPECREW_REFOCUS_DISABLE) $lines.Add((" env SPECREW_REFOCUS_DISABLE: {0}" -f $(if ($envDisabled) { 'SET (all hook triggers silenced)' } else { 'not set' }))) | Out-Null $catalog = Get-RefocusCatalog -ProjectRoot $ProjectRoot if ($null -ne $catalog -and $catalog.PSObject.Properties['triggers'] -and $null -ne $catalog.triggers) { foreach ($prop in $catalog.triggers.PSObject.Properties) { $enabled = $true if ($prop.Value.PSObject.Properties['enabled']) { $enabled = [bool]$prop.Value.enabled } $lines.Add((" trigger {0}: {1}" -f $prop.Name, $(if ($enabled) { 'enabled' } else { 'DISABLED (catalog)' }))) | Out-Null } } else { $lines.Add(' catalog: unavailable (see WARN above)') | Out-Null } $stateFiles = @(Get-RefocusRuntimeStateFiles -ProjectRoot $ProjectRoot) if ($stateFiles.Count -eq 0) { $lines.Add(' sessions: no runtime state recorded yet') | Out-Null } foreach ($file in $stateFiles) { try { $state = Get-Content -LiteralPath $file.FullName -Raw -Encoding UTF8 | ConvertFrom-Json } catch { $lines.Add((" session {0}: state unreadable" -f $file.Name)) | Out-Null continue } $trip = 'no trips' if ($state.PSObject.Properties['breaker'] -and $null -ne $state.breaker -and $state.breaker.PSObject.Properties['tripped'] -and [bool]$state.breaker.tripped) { $trip = ("TRIPPED ({0})" -f [string]$state.breaker.reason) } $journalCount = 0 if ($state.PSObject.Properties['journal'] -and $null -ne $state.journal) { $journalCount = @($state.journal).Count } $lines.Add((" session {0}: breaker {1}; journal entries {2}" -f $file.Name, $trip, $journalCount)) | Out-Null foreach ($entry in @(if ($journalCount -gt 0) { $state.journal | Select-Object -Last 5 } else { @() })) { $lines.Add((" {0} {1} {2} {3} tokens~{4} {5}" -f [string]$entry.at, [string]$entry.trigger, [string]$entry.scope, [string]$entry.channel, [string]$entry.tokens, [string]$entry.outcome)) | Out-Null } } return ($lines -join "`n") } function Invoke-RefocusResetBreaker { param([Parameter(Mandatory = $true)][string]$ProjectRoot) $cleared = 0 foreach ($file in @(Get-RefocusRuntimeStateFiles -ProjectRoot $ProjectRoot)) { try { $state = Get-Content -LiteralPath $file.FullName -Raw -Encoding UTF8 | ConvertFrom-Json if ($state.PSObject.Properties['breaker'] -and $null -ne $state.breaker -and $state.breaker.PSObject.Properties['tripped'] -and [bool]$state.breaker.tripped) { $state.breaker = $null $json = $state | ConvertTo-Json -Depth 8 [System.IO.File]::WriteAllText($file.FullName, $json, [System.Text.UTF8Encoding]::new($false)) $cleared++ } } catch { Write-RefocusWarn -Code 'STATE_UNAVAILABLE' -Message ("could not reset breaker in {0}: {1}" -f $file.Name, $_.Exception.Message) } } return ("{0} breaker reset: {1} trip flag(s) cleared" -f $script:Banner, $cleared) } # --------------------------------------------------------------------------- # Argument parsing (GNU-style flags so the slash surface reads naturally). # --------------------------------------------------------------------------- $projectRoot = Get-RefocusProjectRoot if ($null -eq $projectRoot) { Write-RefocusWarn -Code 'STATE_UNAVAILABLE' -Message 'no .specrew project found from the current directory; refocus is a Specrew-project surface' exit 0 } $tokens = @($Arguments | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) $mode = 'default' $modeValue = $null if ($tokens.Count -gt 0) { switch -Regex ($tokens[0]) { '^--boundary$' { if ($tokens.Count -lt 2) { Write-RefocusWarn -Code 'EVENT_PARSE' -Message '--boundary requires a stage name'; exit 2 } $mode = 'boundary'; $modeValue = $tokens[1]; break } '^--role$' { if ($tokens.Count -lt 2) { Write-RefocusWarn -Code 'EVENT_PARSE' -Message '--role requires a role name'; exit 2 } $mode = 'role'; $modeValue = $tokens[1]; break } '^--trigger$' { if ($tokens.Count -lt 2 -or $tokens[1] -notin @('b1', 'b2', 'b3')) { Write-RefocusWarn -Code 'EVENT_PARSE' -Message '--trigger requires b1|b2|b3'; exit 2 } $mode = 'trigger'; $modeValue = $tokens[1]; break } '^--shape-catalog$' { $mode = 'shape-catalog'; break } '^--everything$' { $mode = 'everything'; break } '^--compact-instructions$' { $mode = 'compact-instructions'; break } '^--status$' { $mode = 'status'; break } '^--reset-breaker$' { $mode = 'reset-breaker'; break } default { Write-RefocusWarn -Code 'EVENT_PARSE' -Message ("unknown argument '{0}'; valid: --boundary <stage> | --role <name> | --trigger <b1|b2|b3> | --shape-catalog | --everything | --compact-instructions | --status | --reset-breaker" -f $tokens[0]) exit 2 } } } switch ($mode) { 'default' { $boundary = Get-RefocusCurrentBoundary -StartContext (Get-RefocusStartContext -ProjectRoot $projectRoot) $scopes = @('general') if (-not [string]::IsNullOrWhiteSpace($boundary)) { $scopes += ("boundary.{0}" -f $boundary) } Invoke-RefocusScopePayload -ProjectRoot $projectRoot -Trigger 'manual' -ScopeIds $scopes } 'boundary' { Invoke-RefocusScopePayload -ProjectRoot $projectRoot -Trigger 'manual' -ScopeIds @('general', ("boundary.{0}" -f $modeValue)) } 'role' { Invoke-RefocusRoleScope -ProjectRoot $projectRoot -RoleName $modeValue } 'trigger' { $catalog = Get-RefocusCatalog -ProjectRoot $projectRoot $scopes = @('general') if ($null -ne $catalog -and $catalog.PSObject.Properties['triggers'] -and $null -ne $catalog.triggers -and $catalog.triggers.PSObject.Properties[$modeValue]) { $entry = $catalog.triggers.$modeValue # Durable per-trigger disable (kill-switch level: catalog flag). Silence # is the OPERATOR'S intent here — no payload, no warning, exit 0; the # disable is visible via --status. Applies to every channel that uses # trigger semantics (wrapper emission + hook providers); the manual # --boundary/--role surfaces are unaffected. if ($entry.PSObject.Properties['enabled'] -and -not [bool]$entry.enabled) { exit 0 } if ($entry.PSObject.Properties['scopes'] -and $null -ne $entry.scopes) { $scopes = @($entry.scopes | ForEach-Object { [string]$_ }) } } else { # Trigger missing from catalog: fall back to the default composition so # the trigger layer still delivers something useful (fail-open). $boundary = Get-RefocusCurrentBoundary -StartContext (Get-RefocusStartContext -ProjectRoot $projectRoot) if (-not [string]::IsNullOrWhiteSpace($boundary)) { $scopes += ("boundary.{0}" -f $boundary) } } # Resolve dynamic placeholders the trigger map may carry. $boundaryNow = Get-RefocusCurrentBoundary -StartContext (Get-RefocusStartContext -ProjectRoot $projectRoot) $boundaryNext = Get-RefocusBoundarySuccessor -Boundary $boundaryNow $scopes = @($scopes | ForEach-Object { if ($_ -eq 'boundary.current' -and -not [string]::IsNullOrWhiteSpace($boundaryNow)) { "boundary.{0}" -f $boundaryNow } elseif ($_ -eq 'boundary.next' -and -not [string]::IsNullOrWhiteSpace($boundaryNext)) { "boundary.{0}" -f $boundaryNext } elseif ($_ -in @('boundary.current', 'boundary.next')) { 'general' } else { $_ } } | Select-Object -Unique) Invoke-RefocusScopePayload -ProjectRoot $projectRoot -Trigger $modeValue -ScopeIds $scopes } 'shape-catalog' { Invoke-RefocusScopePayload -ProjectRoot $projectRoot -Trigger 'manual' -ScopeIds @('shape-catalog') } 'everything' { $catalog = Get-RefocusCatalog -ProjectRoot $projectRoot $scopes = @('general') if ($null -ne $catalog -and $catalog.PSObject.Properties['scopes'] -and $null -ne $catalog.scopes) { $scopes = @($catalog.scopes.PSObject.Properties | ForEach-Object { $_.Name }) } Write-RefocusWarn -Code 'BUDGET_EXCEEDED' -Message '--everything loads the full digest corpus; expect a heavy payload' Invoke-RefocusScopePayload -ProjectRoot $projectRoot -Trigger 'manual' -ScopeIds $scopes } 'compact-instructions' { Invoke-RefocusCompactInstructions -ProjectRoot $projectRoot } 'status' { Invoke-RefocusStatus -ProjectRoot $projectRoot } 'reset-breaker' { Invoke-RefocusResetBreaker -ProjectRoot $projectRoot } } exit 0 |