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 |