hosts/codex/handlers.ps1

# Codex host package — handler implementations
#
# Per hosts/_contract.md, exposes the 4 contract functions:
# - New-CodexLaunchInvocation
# - ConvertTo-CodexFlag
# - Test-CodexRuntimeInstalled
# - Get-CodexSignals
#
# Extracted Phase B from:
# - scripts/specrew-start.ps1 Get-SpecrewHostLaunchInvocation (Codex arm)
# - scripts/internal/host-flag-translation.ps1 Get-HostFlagTranslation (Codex arms)
# - scripts/internal/host-runtime-inventory.ps1 Test-CodexRuntimeInstalled
#
# Behavior IDENTICAL to the extracted source.
#
# Codex launch shape (verified 2026-05-23 real-launch test + 2026-05-24 deep review):
# interactive REPL uses positional prompt (`codex "<prompt>" --cd <path>`). `codex exec`
# is the batch / non-interactive subcommand and is NOT used by F-040.
#
# Codex flag note (verified 2026-05-24 via `codex --help`): `--full-auto` is deprecated
# AND is `codex exec`-only. The full-equivalent of Claude's --dangerously-skip-permissions
# is `--dangerously-bypass-approvals-and-sandbox` (long form of `--yolo`).

Set-StrictMode -Version Latest

function New-CodexLaunchInvocation {
    <#
    .SYNOPSIS
    Build the Codex CLI launch invocation per F-040 research.md Task 1.
    .OUTPUTS
    pscustomobject @{ Binary; Args[]; Notices[]; HostKind = 'codex' }
    #>

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

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

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

    $argList.Add('--cd') | Out-Null
    $argList.Add($ProjectPath) | Out-Null

    if ($AllowAll) {
        $t = ConvertTo-CodexFlag -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-CodexFlag -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-CodexFlag -SpecrewFlag '--remote'
        # Codex has no remote-control wiring; surface notice but inject no args
        if (-not [string]::IsNullOrWhiteSpace($t.Notice)) { $notices.Add($t.Notice) | Out-Null }
    }

    # Positional prompt is LAST (interactive launch; not `codex exec`)
    $argList.Add($Prompt) | Out-Null

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

function ConvertTo-CodexFlag {
    <#
    .SYNOPSIS
    Translate a Specrew-side flag to Codex 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            = @()
                Notice          = 'Codex CLI does not expose a remote-control flag today; continuing launch without remote-control wiring.'
                SuppressWarning = $false
            }
        }
        '--allow-all' {
            return [pscustomobject]@{
                Args            = @('--dangerously-bypass-approvals-and-sandbox')
                Notice          = "Translated --allow-all to Codex's --dangerously-bypass-approvals-and-sandbox flag (full equivalent of Claude's --dangerously-skip-permissions)."
                SuppressWarning = $true
            }
        }
        '--autopilot' {
            return [pscustomobject]@{
                Args            = @()
                Notice          = "Codex's autopilot equivalent is --dangerously-bypass-approvals-and-sandbox, which is already mapped from --allow-all. --autopilot is a no-op when --allow-all is also set."
                SuppressWarning = $true
            }
        }
    }
}

function Test-CodexRuntimeInstalled {
    <#
    .SYNOPSIS
    Codex's Crew runtime is .codex/agents/ TOML files (Proposal 024 Slice 3).
    F-043 only detects; does not deploy.
    .OUTPUTS
    bool
    #>

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

function Get-CodexSignals {
    <#
    .SYNOPSIS
    Detect Codex-set environment variables.
    .OUTPUTS
    string[] — names of env vars that are set
    #>

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

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

function ConvertTo-CodexTomlString {
    # Minimal TOML triple-quoted string with backslash + triple-quote escaping
    param([string]$Value)
    $escaped = $Value -replace '\\', '\\\\'
    $escaped = $escaped -replace '"""', '\"\"\"'
    return ('"""{0}{1}{0}"""' -f [Environment]::NewLine, $escaped)
}

function Install-CodexCrewRuntime {
    <#
    .SYNOPSIS
    Deploy Specrew's Crew runtime to .codex/agents/<role>.toml from canonical .specrew/team/agents/<role>.md.
    Proposal 108 Slice 9 contract function.
    .DESCRIPTION
    Translates each canonical role-charter into Codex CLI's subagent file format:
    .codex/agents/<role>.toml with required `name`, `description`, `developer_instructions` fields.
    The charter markdown body becomes the developer_instructions multi-line TOML string.
    Reference: https://developers.openai.com/codex/subagents
    .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]
    $codexAgentsRoot = Get-SpecrewHostAgentRoot -HostKind 'codex' -ProjectPath $ProjectPath
    if (-not (Test-Path -LiteralPath $codexAgentsRoot -PathType Container) -and -not $DryRun) {
        New-Item -ItemType Directory -Path $codexAgentsRoot -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-CodexAgentDescription -Charter $content -Role $role
        $developerInstructions = ConvertTo-CodexTomlString -Value $content
        # Codex TOML: name + description + developer_instructions required
        $tomlLines = @(
            ('# Specrew-managed: this Codex 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),
            '',
            ('name = "{0}"' -f $role),
            ('description = "{0}"' -f ($description -replace '\\', '\\\\' -replace '"', '\"')),
            ('developer_instructions = {0}' -f $developerInstructions),
            ''
        )
        $toml = $tomlLines -join "`n"

        $target = Join-Path $codexAgentsRoot ("{0}.toml" -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
        }
        if ($DryRun) {
            $actions.Add(@{ Action = 'would-write'; Path = $target; Role = $role }) | Out-Null
        }
        else {
            [System.IO.File]::WriteAllText($target, $toml, [System.Text.UTF8Encoding]::new($false))
            $actions.Add(@{ Action = 'written'; Path = $target; Role = $role }) | Out-Null
        }
    }

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