scripts/internal/continuous-co-review/host-support-doctor.ps1

$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest

# F-198 Iteration 005 / T039 (FR-050 + FR-053 + FR-051): the doctor/status AGGREGATOR.
#
# WHY THIS FILE EXISTS: the three Beta2 host-integration truths a maintainer needs at a glance are produced by
# three separate leaf modules -
# * the host+surface support TIERS (host-support-tier.ps1 -> Format-SpecrewHostSupportTierReport)
# * the hook-health EVIDENCE (hook-health-receipt.ps1 -> Format-SpecrewHookHealthReport)
# * the Codex untrusted-headless PREFLIGHT (hook-health-receipt.ps1 -> Test-SpecrewCodexHeadlessGovernanceReady)
# This module is the ONE narrow seam that stitches all three into a single doctor/status STRING, so a surface
# (see below) adds ONE call instead of wiring three renderers + a preflight and formatting the preflight itself.
#
# F-184 FOOTPRINT: NONE. This is a NEW, non-protected leaf module. It exists PRECISELY so the protected doctor
# surface (scripts/specrew-hooks.ps1) never has to be edited to gain three renderers: that protected file can, at
# the maintainer's discretion, add exactly ONE dot-source + ONE call (documented at the bottom of this file) and
# render everything. It is ALSO directly callable by any non-protected status path. Pure string building + the
# read-only resolvers of its two siblings; it NEVER writes a receipt, a config, or ~/.codex.
#
# HONESTY (NFR-001): this aggregator only FORMATS what its siblings return - it can never upgrade a tier or a
# health status. A missing/stale/malformed receipt stays unverified/degraded; an unknown host/surface stays
# unverified; the Codex preflight stays NOT-ready without a current receipt. There is no health-washing seam here.
#
# FAIL-OPEN: a doctor report must never throw. If a sibling renderer is unavailable (module not loaded) the
# corresponding section degrades to a single honest note rather than crashing the status surface.

# Best-effort load of the two sibling leaf modules so this aggregator is DROP-IN for a surface that has not
# already dot-sourced them (e.g. the protected specrew-hooks.ps1). Fail-open: a resolve/dot-source failure just
# leaves the corresponding Get-Command guard false, and that section degrades to a note.
foreach ($sibling in @('host-support-tier.ps1', 'hook-health-receipt.ps1')) {
    try {
        $siblingPath = Join-Path $PSScriptRoot $sibling
        if (Test-Path -LiteralPath $siblingPath -PathType Leaf) { . $siblingPath }
    }
    catch { $null = $_ }
}

function Format-SpecrewCodexHeadlessReadinessReport {
    # Render the Codex untrusted-headless governance PREFLIGHT (FR-051) as a doctor/status STRING. Consults the
    # read-only Test-SpecrewCodexHeadlessGovernanceReady (which NEVER writes ~/.codex). Returns a STRING; never
    # throws. NEVER reports ready on a missing/stale/malformed/drifted receipt - it renders exactly what the
    # preflight returned (no ready-washing).
    param(
        [Parameter(Mandatory)][string]$ProjectRoot,
        [AllowNull()][string]$ExpectedHostVersion,
        [AllowNull()][datetime]$Now
    )
    $sb = [System.Text.StringBuilder]::new()
    [void]$sb.AppendLine('=== Codex untrusted-headless governance preflight (FR-051) ===')
    if (-not (Get-Command -Name 'Test-SpecrewCodexHeadlessGovernanceReady' -ErrorAction SilentlyContinue)) {
        [void]$sb.AppendLine(' (unavailable: hook-health-receipt module not loaded)')
        return $sb.ToString()
    }
    try {
        $preArgs = @{ ProjectRoot = $ProjectRoot }
        if ($PSBoundParameters.ContainsKey('ExpectedHostVersion') -and -not [string]::IsNullOrWhiteSpace($ExpectedHostVersion)) { $preArgs.ExpectedHostVersion = $ExpectedHostVersion }
        if ($PSBoundParameters.ContainsKey('Now')) { $preArgs.Now = $Now }
        $pre = Test-SpecrewCodexHeadlessGovernanceReady @preArgs
        [void]$sb.AppendLine((' codex/cli ready to govern a headless run: {0} (hook liveness: {1}; version diagnostic: {2})' -f $(if ($pre.ready) { 'YES' } else { 'NO' }), $pre.hook_status, $pre.version_status))
        [void]$sb.AppendLine(' (readiness is operational confidence from fresh hook-liveness monitoring, not tamper-proof host authentication)')
        [void]$sb.AppendLine((' reason: {0}' -f $pre.reason))
        [void]$sb.AppendLine((' next step: {0}' -f $pre.instruction))
    }
    catch {
        [void]$sb.AppendLine((' (preflight error, treated as NOT ready: {0})' -f $_.Exception.Message))
    }
    return $sb.ToString()
}

function Format-SpecrewHostSupportDoctorReport {
    # THE aggregator: one doctor/status STRING = host-support tiers + hook-health evidence + the Codex headless
    # preflight, in that order, each separated by a blank line. Given a ProjectRoot it resolves live hook-health;
    # -Hosts narrows the health rows (default: the CLI-first gated hosts). Never throws (fail-open per section).
    param(
        [Parameter(Mandatory)][string]$ProjectRoot,
        [string[]]$Hosts,
        [AllowNull()][datetime]$Now
    )
    $sections = New-Object System.Collections.Generic.List[string]

    # 1. host+surface support tiers (FR-050).
    if (Get-Command -Name 'Format-SpecrewHostSupportTierReport' -ErrorAction SilentlyContinue) {
        try { $sections.Add((Format-SpecrewHostSupportTierReport).TrimEnd()) | Out-Null }
        catch { $sections.Add('=== Specrew Beta2 host-support tiers ===' + [Environment]::NewLine + (' (unavailable: {0})' -f $_.Exception.Message)) | Out-Null }
    }
    else {
        $sections.Add('=== Specrew Beta2 host-support tiers ===' + [Environment]::NewLine + ' (unavailable: host-support-tier module not loaded)') | Out-Null
    }

    # 2. hook-health evidence (FR-053).
    if (Get-Command -Name 'Format-SpecrewHookHealthReport' -ErrorAction SilentlyContinue) {
        try {
            $healthArgs = @{ ProjectRoot = $ProjectRoot }
            if ($null -ne $Hosts -and @($Hosts).Count -gt 0) { $healthArgs.Hosts = @($Hosts) }
            if ($PSBoundParameters.ContainsKey('Now')) { $healthArgs.Now = $Now }
            $sections.Add((Format-SpecrewHookHealthReport @healthArgs).TrimEnd()) | Out-Null
        }
        catch { $sections.Add('=== Specrew hook-health evidence (FR-053) ===' + [Environment]::NewLine + (' (unavailable: {0})' -f $_.Exception.Message)) | Out-Null }
    }
    else {
        $sections.Add('=== Specrew hook-health evidence (FR-053) ===' + [Environment]::NewLine + ' (unavailable: hook-health-receipt module not loaded)') | Out-Null
    }

    # 3. Codex untrusted-headless preflight (FR-051).
    $preflightArgs = @{ ProjectRoot = $ProjectRoot }
    if ($PSBoundParameters.ContainsKey('Now')) { $preflightArgs.Now = $Now }
    $sections.Add((Format-SpecrewCodexHeadlessReadinessReport @preflightArgs).TrimEnd()) | Out-Null

    return (($sections -join ([Environment]::NewLine + [Environment]::NewLine)) + [Environment]::NewLine)
}

# ------------------------------------------------------------------------------------------------------------
# PRODUCTION SURFACING (F-198 iter-005 finding 4 - RESOLVED). This aggregator is WIRED into a real user command:
#
# specrew hooks doctor
#
# routed by the non-protected CLI dispatcher (scripts/specrew.ps1, `hooks` arm) to the non-protected script
# scripts/specrew-hooks-doctor.ps1, which dot-sources THIS module and calls Format-SpecrewHostSupportDoctorReport.
# That seam was chosen because the natural home (`specrew hooks status` in scripts/specrew-hooks.ps1) and the
# hook inspector (scripts/internal/specrew-hook-health.ps1) are BOTH F-184-protected surfaces (enforced by
# tests/continuous-co-review/governance/protected-surface-guard.Tests.ps1), so editing them for surfacing was not
# authorized. `specrew hooks doctor` reaches this render WITHOUT touching any protected file. The command path is
# covered end-to-end by tests/integration/f198-iter005-hook-health-production-path.tests.ps1.
#
# Any OTHER non-protected status path may surface the same report with a single self-contained call (this module
# loads its own two siblings fail-open):
# . (Join-Path $PSScriptRoot 'internal/continuous-co-review/host-support-doctor.ps1')
# Write-Host (Format-SpecrewHostSupportDoctorReport -ProjectRoot $projectPath)
# ------------------------------------------------------------------------------------------------------------