hosts/copilot/handlers.ps1
|
# Copilot host package — handler implementations # # Per hosts/_contract.md, exposes the 4 contract functions: # - New-CopilotLaunchInvocation # - ConvertTo-CopilotFlag # - Test-CopilotRuntimeInstalled # - Get-CopilotSignals # # Extracted Phase B from: # - scripts/specrew-start.ps1 Get-SpecrewHostLaunchInvocation (Copilot arm) # - scripts/internal/host-flag-translation.ps1 Get-HostFlagTranslation (Copilot arms) # - scripts/internal/host-runtime-inventory.ps1 Test-CopilotRuntimeInstalled # - scripts/specrew-init.ps1 Get-CopilotSignals # # Behavior IDENTICAL to the extracted source. Legacy functions remain as # thin shims during Phase B; final cleanup removes shims in a later phase. Set-StrictMode -Version Latest function New-CopilotLaunchInvocation { <# .SYNOPSIS Build the Copilot CLI launch invocation per F-040 research.md Task 1. .OUTPUTS pscustomobject @{ Binary; Args[]; Notices[]; HostKind = 'copilot' } #> param( [Parameter(Mandatory = $true)][string]$ProjectPath, [Parameter(Mandatory = $true)][string]$Prompt, [Parameter(Mandatory = $true)][string]$Agent, [bool]$AllowAll = $false, [bool]$UseAutopilot = $false, [bool]$UseRemote = $false ) $hostCmd = Get-Command 'copilot' -ErrorAction SilentlyContinue $resolvedBinary = if ($null -ne $hostCmd) { $hostCmd.Source } else { 'copilot' } $argList = New-Object System.Collections.Generic.List[string] $notices = New-Object System.Collections.Generic.List[string] $argList.Add('--agent') | Out-Null $argList.Add($Agent) | Out-Null if ($UseAutopilot) { $t = ConvertTo-CopilotFlag -SpecrewFlag '--autopilot' foreach ($a in $t.Args) { $argList.Add($a) | Out-Null } } $argList.Add('--add-dir') | Out-Null $argList.Add($ProjectPath) | Out-Null $argList.Add('-i') | Out-Null $argList.Add($Prompt) | Out-Null if ($AllowAll) { $t = ConvertTo-CopilotFlag -SpecrewFlag '--allow-all' foreach ($a in $t.Args) { $argList.Add($a) | Out-Null } } if ($UseRemote) { $t = ConvertTo-CopilotFlag -SpecrewFlag '--remote' foreach ($a in $t.Args) { $argList.Add($a) | Out-Null } } return [pscustomobject]@{ Binary = $resolvedBinary Args = $argList.ToArray() Notices = $notices.ToArray() HostKind = 'copilot' } } function ConvertTo-CopilotFlag { <# .SYNOPSIS Translate a Specrew-side flag to Copilot CLI flag(s). .OUTPUTS pscustomobject @{ Args[]; Notice; SuppressWarning } #> param( [Parameter(Mandatory = $true)] [ValidateSet('--remote', '--allow-all', '--autopilot')] [string]$SpecrewFlag ) switch ($SpecrewFlag) { '--remote' { return [pscustomobject]@{ Args = @('--remote'); Notice = ''; SuppressWarning = $true } } '--allow-all' { return [pscustomobject]@{ Args = @('--allow-all'); Notice = ''; SuppressWarning = $true } } '--autopilot' { return [pscustomobject]@{ Args = @('--autopilot'); Notice = ''; SuppressWarning = $true } } } } function Test-CopilotRuntimeInstalled { <# .SYNOPSIS Copilot's Crew runtime is Squad. Detect via .squad/ directory. .OUTPUTS bool #> param([Parameter(Mandatory = $true)][string]$ProjectPath) $squadDir = Join-Path $ProjectPath '.squad' return [bool](Test-Path -LiteralPath $squadDir -PathType Container) } function Get-CopilotSignals { <# .SYNOPSIS Detect Copilot-set environment variables (run-time host context). .OUTPUTS string[] — names of env vars that are set #> $signals = @() foreach ($variableName in @('COPILOT_CLI', 'COPILOT_AGENT_SESSION_ID', 'COPILOT_CLI_BINARY_VERSION')) { $value = [Environment]::GetEnvironmentVariable($variableName) if (-not [string]::IsNullOrWhiteSpace($value)) { $signals += $variableName } } return $signals } function Install-CopilotCrewRuntime { <# .SYNOPSIS Deploy the Crew runtime to .squad/agents/<role>/charter.md from canonical .specrew/team/agents/<role>.md. Proposal 108 Slice 9 contract function. .DESCRIPTION Squad CLI reads from .squad/agents/<role>/charter.md as its native location. This function TRANSLATES the canonical .specrew/team/agents/<role>.md charters to that location. Squad's other state (config.json, team.md, ceremonies.md) is bootstrapped by 'squad init' or Initialize-SquadFallbackScaffold and is OUTSIDE this function's scope — only the per-role charter.md files (= the team identity) are translated here. .OUTPUTS pscustomobject @{ Actions[]; CrewRuntimePath; Notices[] } #> param( [Parameter(Mandatory = $true)][string]$ProjectPath, [switch]$DryRun ) $actions = New-Object System.Collections.Generic.List[hashtable] $notices = New-Object System.Collections.Generic.List[string] $squadAgentsRoot = Get-SpecrewHostAgentRoot -HostKind 'copilot' -ProjectPath $ProjectPath foreach ($role in (Get-SpecrewCanonicalAgentRoles -ProjectPath $ProjectPath)) { $roleDir = Join-Path $squadAgentsRoot $role if (-not (Test-Path -LiteralPath $roleDir -PathType Container) -and -not $DryRun) { New-Item -ItemType Directory -Path $roleDir -Force | Out-Null } $content = Get-SpecrewCanonicalCharterContent -ProjectPath $ProjectPath -RoleName $role if ([string]::IsNullOrWhiteSpace($content)) { $notices.Add("Skipping role '$role': no canonical charter at .specrew/team/agents/$role.md and no shipped baseline.") | Out-Null continue } $charterPath = Join-Path $roleDir 'charter.md' if (-not (Test-SpecrewManagedFile -Path $charterPath)) { $notices.Add("Preserving user-edited file '$charterPath' (no Specrew-managed marker; delete it OR delete the sidecar '$charterPath.specrew-managed' to re-sync from canonical).") | Out-Null $actions.Add(@{ Action = 'preserved'; Path = $charterPath; Role = $role }) | Out-Null continue } # Copilot consumes charter.md as the charter body verbatim (no frontmatter / comment header). # Use a sidecar marker file instead of an inline comment so Squad CLI parsing isn't affected. if ($DryRun) { $actions.Add(@{ Action = 'would-write'; Path = $charterPath; Role = $role }) | Out-Null } else { [System.IO.File]::WriteAllText($charterPath, $content, [System.Text.UTF8Encoding]::new($false)) Write-SpecrewManagedSidecar -Path $charterPath $actions.Add(@{ Action = 'written'; Path = $charterPath; Role = $role }) | Out-Null } } return [pscustomobject]@{ Actions = $actions.ToArray() CrewRuntimePath = $squadAgentsRoot Notices = $notices.ToArray() } } |