Payload/scripts/harness/harness-health.ps1

#Requires -Version 5.1
<#
.SYNOPSIS
    One-command harness status check for agents.

.DESCRIPTION
    Prints a concise status summary for a feature/slice: ownership_status,
    latest turn start/stop, latest verify-local verdict, phase completion,
    journal event count, and a non-RequireReviewReady validation. Run any time
    you are uncertain about harness state instead of manually reading the journal.

    Output is human-readable and also emitted as a JSON object so agents can
    parse it programmatically.

.EXAMPLE
    pwsh -File scripts/harness-invoke.ps1 harness-health.ps1 -Feature saved-search-queries -Slice mo-save-list
#>

[CmdletBinding()]
param(
    [Parameter(Mandatory = $true)]
    [string]$Feature,

    [Parameter(Mandatory = $true)]
    [string]$Slice,

    [string]$FeatureRoot = '',

    [string]$RepoRoot = '',

    [int]$StaleTurnMinutes = 8,

    [switch]$AllowArbitraryFeatureRoot
)

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

Import-Module (Join-Path $PSScriptRoot 'lib\HarnessContract.psm1') -Force
Import-Module (Join-Path $PSScriptRoot 'lib\HarnessJournal.psm1') -Force
Import-Module (Join-Path $PSScriptRoot 'lib\HarnessGateProjection.psm1') -Force
Test-HarnessContractModuleReady

if ([string]::IsNullOrWhiteSpace($RepoRoot)) {
    $RepoRoot = Get-HarnessRepoRoot -StartPath $PSScriptRoot
}

$featureRootPath = Get-HarnessFeatureRoot -RepoRoot $RepoRoot -Feature $Feature `
    -FeatureRootOverride $FeatureRoot -AllowArbitraryFeatureRoot:$AllowArbitraryFeatureRoot

# -- Journal ------------------------------------------------------------------
$journalPath = Join-Path $featureRootPath 'journal\events.jsonl'
$allEvents = @()
if (Test-Path -LiteralPath $journalPath) {
    $allEvents = @(Get-HarnessJournalEvents -FeatureRoot $featureRootPath)
}
$sliceEvents = @($allEvents | Where-Object { $_.slice -eq $Slice } | Sort-Object { [int](Get-HarnessProperty -Object $_ -Name 'seq' -Default 0) })

# Turn windows
$starts = @($sliceEvents | Where-Object { (Get-HarnessProperty -Object $_ -Name 'kind') -eq 'ActivityStarted' })
$stops  = @($sliceEvents | Where-Object { (Get-HarnessProperty -Object $_ -Name 'kind') -eq 'ActivityStopped' })
$latestStart = if ($starts.Count -gt 0) { $starts[-1] } else { $null }
$latestStop  = if ($stops.Count  -gt 0) { $stops[-1]  } else { $null }

# Latest verify-local verdict from journal
$verifyEvents = @($sliceEvents | Where-Object {
        (Get-HarnessProperty -Object $_ -Name 'kind') -eq 'SensorRunRecorded' `
            -and (Get-HarnessProperty -Object (Get-HarnessProperty -Object $_ -Name 'payload') -Name 'name') -eq 'verify-local'
    })
$latestVerify = if ($verifyEvents.Count -gt 0) { $verifyEvents[-1] } else { $null }
$latestVerifyVerdict = if ($latestVerify) { Get-HarnessProperty -Object $latestVerify -Name 'outcome' } else { 'none' }
$latestVerifySeq     = if ($latestVerify) { [int](Get-HarnessProperty -Object $latestVerify -Name 'seq' -Default 0) } else { 0 }

# Baseline discipline quick-check (latest turn window)
$baselineOk = $false
$closeoutOk = $false
if ($latestStart) {
    $latestStartSeq = [int](Get-HarnessProperty -Object $latestStart -Name 'seq' -Default 0)
    $baselineOk = Test-HarnessTurnBaselineSatisfied -OrderedEvents @($sliceEvents) -LatestStartSeq $latestStartSeq -FeatureRoot $featureRootPath
    if ($latestStop) {
        $latestStopSeq = [int](Get-HarnessProperty -Object $latestStop -Name 'seq' -Default 0)
        $beforeStop = @($sliceEvents | Where-Object { [int](Get-HarnessProperty -Object $_ -Name 'seq' -Default 0) -lt $latestStopSeq } | Select-Object -Last 1)
        if ($beforeStop.Count -gt 0) {
            $e = $beforeStop[0]
            $closeoutOk = ((Get-HarnessProperty -Object $e -Name 'kind') -eq 'SensorRunRecorded') `
                -and ((Get-HarnessProperty -Object (Get-HarnessProperty -Object $e -Name 'payload') -Name 'name') -eq 'verify-local') `
                -and ((Get-HarnessProperty -Object $e -Name 'outcome') -eq 'PASS')
        }
    }
}

# -- Projection ----------------------------------------------------------------
$loopPath = Join-Path $featureRootPath '.review-loop.json'
$ownershipStatus = 'unknown'
$phases = @{}
$sliceShapeRepaired = $false
if (Test-Path -LiteralPath $loopPath) {
    $loop = ConvertFrom-HarnessJson -Json (Get-Content $loopPath -Raw)
    $beforeId = $null
    $implBefore = Get-ImplementationPhase -LoopData $loop
    if ($implBefore -and $implBefore.slices) {
        $raw = @($implBefore.slices) | Select-Object -First 1
        if ($raw) { $beforeId = Get-HarnessProperty -Object $raw -Name 'id' }
    }
    $loop = Repair-HarnessReviewLoopShapes -LoopData $loop
    $impl = Get-ImplementationPhase -LoopData $loop
    if ($impl) {
        $sliceEntry = @($impl.slices) | Where-Object { $_.id -eq $Slice } | Select-Object -First 1
        if ($sliceEntry) {
            $ownershipStatus = if ($sliceEntry.ownership_status) { $sliceEntry.ownership_status } else { 'unknown' }
            foreach ($sp in @($sliceEntry.phases)) {
                $phases[$sp.id] = $sp.status
            }
            if ([string]::IsNullOrWhiteSpace([string]$beforeId) -and $sliceEntry.id -eq $Slice) {
                $sliceShapeRepaired = $true
            }
        }
    }
}

# -- Quick validation (non-RequireReviewReady) ---------------------------------
$quickReport = Invoke-HarnessReviewLoopValidation -RepoRoot $RepoRoot -FeatureRoot $featureRootPath -SliceId $Slice
$quickVerdict = $quickReport.verdict
$quickFindings = @($quickReport.findings | ForEach-Object { $_.code })

# -- Summary output ------------------------------------------------------------
$turnOpen = ($null -ne $latestStart -and ($null -eq $latestStop -or [int](Get-HarnessProperty -Object $latestStart -Name 'seq' -Default 0) -gt [int](Get-HarnessProperty -Object $latestStop -Name 'seq' -Default 0)))
$staleTurn = $false
$staleDetail = $null
if ($turnOpen) {
    $staleDetail = Get-HarnessStaleTurnDetail -FeatureRoot $featureRootPath -Slice $Slice -StaleTurnMinutes $StaleTurnMinutes
    $staleTurn = [bool]$staleDetail.Stale
}

Write-Output ''
Write-Output "=== Harness Health: $Feature / $Slice ==="
Write-Output " Ownership status : $ownershipStatus"
Write-Output " Turn open : $turnOpen"
if ($staleTurn) {
    $elapsed = [int][Math]::Floor([double]$staleDetail.ElapsedMinutes)
    $lastLabel = [string]$staleDetail.LastLabel
    if ([string]::IsNullOrWhiteSpace($lastLabel)) {
        $lastLabel = [string]$staleDetail.LastKind
    }
    Write-Output " STALE TURN : elapsed $elapsed min; last event $lastLabel"
}
Write-Output " Baseline OK : $baselineOk"
Write-Output " Closeout OK : $closeoutOk"
Write-Output " Latest verify : $latestVerifyVerdict (seq $latestVerifySeq)"
Write-Output " Journal events : $($sliceEvents.Count)"
Write-Output " Phase states : $(($phases.GetEnumerator() | Sort-Object Name | ForEach-Object { "$($_.Key)=$($_.Value)" }) -join ', ')"
Write-Output " Quick validation : $quickVerdict$(if ($quickFindings.Count -gt 0) { " [$($quickFindings -join ', ')]" })"
if ($turnOpen) {
    Write-Output " Operator view : pwsh -File scripts/harness-invoke.ps1 harness-timeline.ps1 -Feature $Feature -Slice $Slice"
    try {
        $processEval = Invoke-HarnessOwnershipSessionEval -FeatureRoot $featureRootPath -SliceId $Slice
        $hintCodes = @($processEval.findings | ForEach-Object { $_.code })
        if ($processEval.verdict -eq 'FAIL' -and $hintCodes.Count -gt 0) {
            Write-Output " Process-eval hint: $($hintCodes -join ', ') (discretionary; not a review_ready gate)"
        }
    }
    catch {
        Write-Verbose "harness-health: process-eval hint unavailable: $($_.Exception.Message)"
    }
}
if ($sliceShapeRepaired) {
    Write-Output " NOTE : slice shell had null id / collapsed slices object -- repaired in memory from current_slice."
    Write-Output " Persist: node scripts/format-review-loop.mjs docs/harness/features/$Feature/.review-loop.json"
}
if ($quickFindings -contains 'SLICE_NOT_FOUND' -and -not $sliceShapeRepaired) {
    Write-Output " HINT : SLICE_NOT_FOUND means implementation.slices[] has no id='$Slice'."
    Write-Output " Do not treat as 'feature missing'. Check .review-loop.json shape (slices must be an array),"
    Write-Output " or ensure the slice via turn-start / Initialize-HarnessProjectionSlice."
}
Write-Output ''

$result = [pscustomobject]@{
    feature              = $Feature
    slice                = $Slice
    ownership_status     = $ownershipStatus
    turn_open            = $turnOpen
    stale_turn            = $staleTurn
    stale_elapsed_minutes = if ($staleDetail) { [int][Math]::Floor([double]$staleDetail.ElapsedMinutes) } else { 0 }
    baseline_ok          = $baselineOk
    closeout_ok          = $closeoutOk
    latest_verify        = $latestVerifyVerdict
    journal_count        = $sliceEvents.Count
    phases               = $phases
    quick_verdict        = $quickVerdict
    quick_findings       = $quickFindings
    slice_shape_repaired = $sliceShapeRepaired
}

$result | ConvertTo-Json -Depth 5