hosts/antigravity/handlers.ps1

# Antigravity host package — handler implementations
#
# Per hosts/_contract.md, exposes the 4 contract functions:
# - New-AntigravityLaunchInvocation
# - ConvertTo-AntigravityFlag
# - Test-AntigravityRuntimeInstalled
# - Get-AntigravitySignals
#
# Status: PROMOTED from deferred to supported via this Phase B extraction
# (per user directive 2026-05-24 Q3: antigravity-followup slice folds into refactor).
#
# Launch shape (verified 2026-05-24 from `agy --help` in user dogfood):
# `agy -i '<prompt>' --add-dir '<path>' [--dangerously-skip-permissions]`
# The earlier antigravity-followup spec FR-005 shape (`-p ... --output-format json --cwd`)
# was wrong: agy CLI rejects `-output-format` and `--cwd`. Actual flag set per the
# user's `agy --help` output:
# --add-dir Add a directory to the workspace (repeatable)
# -i Short alias for --prompt-interactive
# -p Short alias for --print (non-interactive; not used by specrew start)
# --dangerously-skip-permissions Auto-approve all tool permission requests
# Interactive shape (`-i`) matches Claude's launch convention and is what specrew start expects.

Set-StrictMode -Version Latest

function New-AntigravityLaunchInvocation {
    <#
    .SYNOPSIS
    Build the Antigravity CLI launch invocation per F-040 + antigravity-followup spec.
    .OUTPUTS
    pscustomobject @{ Binary; Args[]; Notices[]; HostKind = 'antigravity' }
    #>

    param(
        [Parameter(Mandatory = $true)][string]$ProjectPath,
        [Parameter(Mandatory = $true)][string]$Prompt,
        [Parameter(Mandatory = $true)][string]$Agent,    # ignored; Antigravity has no --agent flag
        [bool]$AllowAll = $false,
        [bool]$UseAutopilot = $false,
        [bool]$UseRemote = $false
    )

    $hostCmd = Get-Command 'agy' -ErrorAction SilentlyContinue
    $resolvedBinary = if ($null -ne $hostCmd) { $hostCmd.Source } else { 'agy' }

    $argList = New-Object System.Collections.Generic.List[string]
    $notices = New-Object System.Collections.Generic.List[string]

    # Interactive launch shape (verified from agy --help): `agy -i '<prompt>' --add-dir '<path>'`
    $argList.Add('-i') | Out-Null
    $argList.Add($Prompt) | Out-Null
    $argList.Add('--add-dir') | Out-Null
    $argList.Add($ProjectPath) | Out-Null

    if ($AllowAll) {
        $t = ConvertTo-AntigravityFlag -SpecrewFlag '--allow-all'
        foreach ($a in $t.Args) { $argList.Add($a) | Out-Null }
        if (-not [string]::IsNullOrWhiteSpace($t.Notice)) { $notices.Add($t.Notice) | Out-Null }
    }
    if ($UseAutopilot) {
        $t = ConvertTo-AntigravityFlag -SpecrewFlag '--autopilot'
        foreach ($a in $t.Args) { $argList.Add($a) | Out-Null }
        if (-not [string]::IsNullOrWhiteSpace($t.Notice)) { $notices.Add($t.Notice) | Out-Null }
    }
    if ($UseRemote) {
        $t = ConvertTo-AntigravityFlag -SpecrewFlag '--remote'
        if (-not [string]::IsNullOrWhiteSpace($t.Notice)) { $notices.Add($t.Notice) | Out-Null }
    }

    return [pscustomobject]@{
        Binary   = $resolvedBinary
        Args     = $argList.ToArray()
        Notices  = $notices.ToArray()
        HostKind = 'antigravity'
    }
}

function ConvertTo-AntigravityFlag {
    <#
    .SYNOPSIS
    Translate a Specrew-side flag to Antigravity CLI flag(s).
    Translations are UNVERIFIED for Antigravity (no verified remote/allow-all/autopilot equivalents);
    each arm warns rather than silently dropping.
    .OUTPUTS
    pscustomobject @{ Args[]; Notice; SuppressWarning }
    #>

    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet('--remote', '--allow-all', '--autopilot')]
        [string]$SpecrewFlag
    )

    switch ($SpecrewFlag) {
        '--remote' {
            return [pscustomobject]@{
                Args            = @()
                Notice          = 'Antigravity CLI does not expose a verified remote-control flag today; continuing launch without remote-control wiring.'
                SuppressWarning = $false
            }
        }
        '--allow-all' {
            return [pscustomobject]@{
                Args            = @('--dangerously-skip-permissions')
                Notice          = "Translated --allow-all to Antigravity's --dangerously-skip-permissions flag (matches Claude's convention; verified from agy --help)."
                SuppressWarning = $true
            }
        }
        '--autopilot' {
            return [pscustomobject]@{
                Args            = @()
                Notice          = "Antigravity has no verified autopilot equivalent; for unattended runs, use Specrew's --autonomous flag for lifecycle boundary control."
                SuppressWarning = $false
            }
        }
    }
}

function Test-AntigravityRuntimeInstalled {
    <#
    .SYNOPSIS
    Antigravity's Crew runtime convention is .agents/agents/ (per antigravity-followup spec).
    Pending Proposal 024 Slice 3 deploy logic; F-043 only detects.
    .OUTPUTS
    bool
    #>

    param([Parameter(Mandatory = $true)][string]$ProjectPath)
    $agentsDir = Join-Path $ProjectPath '.agents\agents'
    if (-not (Test-Path -LiteralPath $agentsDir -PathType Container)) {
        return $false
    }
    $agentFiles = Get-ChildItem -Path $agentsDir -Filter '*.md' -ErrorAction SilentlyContinue
    return ([bool]$agentFiles) -and ($agentFiles.Count -gt 0)
}

function Get-AntigravitySignals {
    <#
    .SYNOPSIS
    Detect Antigravity-set environment variables. Includes Gemini-deadline-relevant vars.
    .OUTPUTS
    string[] — names of env vars that are set
    #>

    $signals = @()
    foreach ($variableName in @('ANTIGRAVITY_API_KEY', 'ANTIGRAVITY_SESSION_ID', 'GOOGLE_AI_SUBSCRIPTION_TIER')) {
        $value = [Environment]::GetEnvironmentVariable($variableName)
        if (-not [string]::IsNullOrWhiteSpace($value)) {
            $signals += $variableName
        }
    }
    return $signals
}

function ConvertTo-AntigravityAgentDescription {
    param([string]$Charter, [string]$Role)
    return (Get-SpecrewCharterTagline -Charter $Charter -Role $Role)
}

function Install-AntigravityCrewRuntime {
    <#
    .SYNOPSIS
    Deploy Specrew's Crew runtime to .agents/agents/<role>.md from canonical .specrew/team/agents/<role>.md.
    Proposal 108 Slice 9 contract function.
    .DESCRIPTION
    Antigravity inherits Gemini CLI's subagent file format: .agents/agents/<role>.md with YAML frontmatter
    (name, description required; tools optional with wildcard support). Translates each canonical role-charter
    accordingly. Reference: https://geminicli.com/docs/core/subagents/
 
    Confidence: medium — Antigravity is still preview-grade as of 2026-05-24 and the public-spec coverage
    of the agent-file format is thinner than Claude/Codex. Smoke-test the result on first use and adjust.
    .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]
    $antigravityAgentsRoot = Get-SpecrewHostAgentRoot -HostKind 'antigravity' -ProjectPath $ProjectPath
    if (-not (Test-Path -LiteralPath $antigravityAgentsRoot -PathType Container) -and -not $DryRun) {
        New-Item -ItemType Directory -Path $antigravityAgentsRoot -Force | Out-Null
    }

    foreach ($role in (Get-SpecrewCanonicalAgentRoles -ProjectPath $ProjectPath)) {
        $content = Get-SpecrewCanonicalCharterContent -ProjectPath $ProjectPath -RoleName $role
        if ([string]::IsNullOrWhiteSpace($content)) {
            $notices.Add("Skipping role '$role': no canonical charter found.") | Out-Null
            continue
        }

        $description = ConvertTo-AntigravityAgentDescription -Charter $content -Role $role
        $frontmatterLines = @(
            '---',
            ('name: {0}' -f $role),
            ('description: {0}' -f ($description -replace '"', '\"')),
            'tools: "*"',
            ('# Specrew-managed: this subagent file is generated from .specrew/team/agents/{0}.md' -f $role),
            ('# DO NOT EDIT HERE. Edit the canonical file at .specrew/team/agents/{0}.md instead.' -f $role),
            '---',
            ''
        )
        $frontmatter = $frontmatterLines -join "`n"

        $target = Join-Path $antigravityAgentsRoot ("{0}.md" -f $role)
        if (-not (Test-SpecrewManagedFile -Path $target)) {
            $notices.Add("Preserving user-edited file '$target' (no Specrew-managed marker; delete the file to re-sync from canonical).") | Out-Null
            $actions.Add(@{ Action = 'preserved'; Path = $target; Role = $role }) | Out-Null
            continue
        }
        $finalContent = $frontmatter + $content

        if ($DryRun) {
            $actions.Add(@{ Action = 'would-write'; Path = $target; Role = $role }) | Out-Null
        }
        else {
            [System.IO.File]::WriteAllText($target, $finalContent, [System.Text.UTF8Encoding]::new($false))
            $actions.Add(@{ Action = 'written'; Path = $target; Role = $role }) | Out-Null
        }
    }

    return [pscustomobject]@{
        Actions          = $actions.ToArray()
        CrewRuntimePath  = $antigravityAgentsRoot
        Notices          = $notices.ToArray()
    }
}