Payload/scripts/kaden/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.

    -Slice is optional: omitting it renders the feature-level outer-loop view
    (one informational journal-sourced line per advisory outer phase under an
    "Outer loop:" header, Journal events over all rows, quick validation with
    the plan-named pending downgrade). Its turn/ownership JSON keys are
    slice-scoped placeholders in that view; read `view` and `outer_phases`.

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

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

.EXAMPLE
    pwsh -File scripts/kaden/harness-invoke.ps1 harness-health.ps1 -Feature harness-event-journal
#>

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

    # Charter Phase 2 (VIEW-01a): -Slice is optional; omitting it renders the
    # feature-level outer-loop view for outer-loop-only features.
    [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)
}
# VIEW-01a: the feature view counts every journal row; the slice view counts rows
# carrying the slice id. Slice-scoped turn/verify/baseline stats exist only when a
# slice is named.
$journalCount = @($allEvents).Count
$sliceEvents = @()
$latestStart = $null
$latestStop = $null
$latestVerifyVerdict = 'none'
$latestVerifySeq = 0
$baselineOk = $false
$closeoutOk = $false
if ($Slice) {
    $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')
            }
        }
    }
}
$viewJournalCount = if ($Slice) { @($sliceEvents).Count } else { $journalCount }

# -- Projection ----------------------------------------------------------------
$loopPath = Join-Path $featureRootPath '.review-loop.json'
$ownershipStatus = 'unknown'
$phases = @{}
$sliceShapeRepaired = $false
$sliceMaterialized = $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) {
            $sliceMaterialized = $true
            $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
            }
        }
    }
}

# -- Slice registration classification ------------------------------------------
# A slice named in a feature-root slice plan but absent from the projection is
# planned-but-unmaterialized, not unknown: the sanctioned recovery is turn-start,
# which materializes the projection shell through the journal. Never infer
# authorization from a plan filename alone or hand-edit .review-loop.json.
# The plan-token scan is shared with validate-review-loop via HarnessContract
# (charter Phase 2 extraction; health consumes the same function).
$outerLoopView = @(Get-HarnessOuterLoopPhaseView -FeatureRoot $featureRootPath -Events $allEvents)
$invokePrefix = 'pwsh -File scripts/kaden/harness-invoke.ps1'
$plannedNotMaterialized = $false
if ($Slice -and -not $sliceMaterialized -and -not $sliceShapeRepaired) {
    $plannedNotMaterialized = Test-HarnessSliceNamedInPlan -FeatureRoot $featureRootPath -SliceId $Slice
}
$sliceRegistration = if (-not $Slice) {
    'FEATURE_VIEW'
} elseif ($sliceMaterialized) {
    'MATERIALIZED'
} elseif ($plannedNotMaterialized) {
    'PLANNED_NOT_MATERIALIZED'
} else {
    'UNKNOWN'
}
# Recovery command placeholders (<cursor|copilot|claude> / <slug>) follow the recorder
# example vocabulary; the operator or agent fills them for the active host.
$nextCommand = $null
if ($sliceRegistration -eq 'PLANNED_NOT_MATERIALIZED' `
        -and (Test-HarnessFeatureSlug -Feature $Feature) `
        -and (Test-HarnessFeatureSlug -Feature $Slice)) {
    $nextCommand = "$invokePrefix harness-turn-start.ps1 -Feature $Feature -Slice $Slice -Phase RED -ModelProvider <cursor|copilot|claude> -ModelSlug <slug>"
}

# -- Quick validation (non-RequireReviewReady) ---------------------------------
$quickReport = Invoke-HarnessReviewLoopValidation -RepoRoot $RepoRoot -FeatureRoot $featureRootPath -SliceId $Slice `
    -AllowPlannedPending -Events $allEvents
$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 ''
if (-not $Slice) {
    # Charter Phase 2 (VIEW-01a): feature-level outer-loop view for
    # outer-loop-only features - no slice scoping; the operator reads the view,
    # never the raw journal file (rows are journal-sourced and sanitized).
    Write-Output "=== Harness Health: $Feature (feature view) ==="
    Write-Output " Journal events : $journalCount"
    Write-Output " Outer loop:"
    foreach ($outerPhase in $outerLoopView) {
        $detailSuffix = ''
        if ($outerPhase.detail) { $detailSuffix = " ($($outerPhase.detail))" }
        Write-Output (" {0}: {1}{2}" -f $outerPhase.phase, $outerPhase.status, $detailSuffix)
    }
} else {
    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 : $invokePrefix 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 ($Slice -and $sliceRegistration -eq 'PLANNED_NOT_MATERIALIZED') {
    Write-Output " Registration : PLANNED_NOT_MATERIALIZED - the slice is named in a slice plan but not yet materialized in the projection."
    Write-Output " NEXT : $nextCommand"
} elseif ($Slice -and $sliceRegistration -eq 'UNKNOWN') {
    Write-Output " HINT : SLICE_NOT_FOUND means implementation.slices[] has no id='$Slice' and no slice plan names it."
    Write-Output " Do not treat as 'feature missing'. Check .review-loop.json shape (slices must be an array),"
    Write-Output " or materialize the slice through harness-turn-start."
}
Write-Output ''

$result = [pscustomobject]@{
    feature              = $Feature
    slice                = $Slice
    view                 = if ($Slice) { 'slice' } else { 'feature' }
    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        = $viewJournalCount
    phases               = $phases
    outer_phases         = $outerLoopView
    quick_verdict        = $quickVerdict
    quick_findings       = $quickFindings
    slice_shape_repaired = $sliceShapeRepaired
    slice_registration   = $sliceRegistration
    next_command         = $nextCommand
}

$result | ConvertTo-Json -Depth 5