<# .SYNOPSIS Reconstruct the state of lights-out runs for Companion polling and Morning Review. .DESCRIPTION Reads the durable registry at ~\.scout\lights-out\runs.json and, for each run, re-derives live facts from git and gnhf's own artifacts. Safe to call from a brand-new Scout session that has no memory of the conversation that launched the run. .EXAMPLE .\check-run.ps1 -Latest .EXAMPLE .\check-run.ps1 -Since 1 -Json #> [CmdletBinding()] param( [string]$Id, [switch]$Latest, [switch]$Active, [int]$Since = 0, [int]$NotesTail = 40, [switch]$Json ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' . (Join-Path $PSScriptRoot 'lib.ps1') $runs = @(Read-LightsOutRuns) if ($runs.Count -eq 0) { if ($Json) { '[]' } else { Write-Host 'No lights-out runs have ever been registered.' } return } $runs = $runs | Sort-Object { [datetime]::Parse($_.startedAt) } -Descending if ($Id) { $runs = @($runs | Where-Object { $_.id -eq $Id }) } if ($Active) { $runs = @($runs | Where-Object { $_.status -in @('running', 'launching', 'finalizing') }) } if ($Since -gt 0) { $cutoff = (Get-Date).AddDays(-$Since) $runs = @($runs | Where-Object { [datetime]::Parse($_.startedAt) -ge $cutoff }) } if ($Latest) { $runs = @($runs | Select-Object -First 1) } $results = foreach ($run in $runs) { $alive = $false if ($run.PSObject.Properties['workerPid'] -and $run.workerPid) { $alive = Test-LightsOutProcessAlive -ProcessId ([int]$run.workerPid) -StartedAtIso $run.startedAt } $runDir = $null if (Test-Path -LiteralPath $run.repoPath) { $runDir = Resolve-LightsOutRunDir -RepoPath $run.repoPath -RunId $run.id } $endState = $null if ($runDir) { $esPath = Join-Path $runDir 'end-state.json' if (Test-Path -LiteralPath $esPath) { try { $endState = Get-Content -LiteralPath $esPath -Raw | ConvertFrom-Json } catch { } } } $commits = @() $diffStat = '' $branchExists = $false if ((Test-Path -LiteralPath $run.repoPath) -and $run.baseCommit) { $branchExists = [bool](git -C $run.repoPath rev-parse --verify --quiet $run.branch 2>$null) $ref = if ($branchExists) { $run.branch } else { 'HEAD' } $commits = @(git -C $run.repoPath --no-pager log --oneline "$($run.baseCommit)..$ref" 2>$null) $diffStat = (git -C $run.repoPath --no-pager diff --shortstat "$($run.baseCommit)..$ref" 2>$null) -join ' ' } $notesTailText = $null if ($runDir) { $np = Join-Path $runDir 'notes.md' if (Test-Path -LiteralPath $np) { $notesTailText = (Get-Content -LiteralPath $np -Tail $NotesTail -ErrorAction SilentlyContinue) -join "`n" } } $elapsed = $null try { $end = if ($run.PSObject.Properties['endedAt'] -and $run.endedAt) { [datetime]::Parse($run.endedAt) } else { Get-Date } $elapsed = '{0:h\hm\m}' -f ($end - [datetime]::Parse($run.startedAt)) } catch { } $vaultNote = $null if ($run.PSObject.Properties['vaultNote'] -and $run.vaultNote) { $vaultNote = $run.vaultNote } else { $guess = Join-Path (Get-LightsOutVaultFolder -Run $run) 'summary.md' if (Test-Path -LiteralPath $guess) { $vaultNote = $guess } } # A run the registry thinks is live, whose worker process is gone, died without finalizing # (reboot, forced kill, power loss). Say so rather than reporting it as still working. $effectiveStatus = $run.status if ($run.status -in @('running', 'launching', 'finalizing') -and -not $alive) { $effectiveStatus = 'orphaned' } [pscustomobject]@{ id = $run.id status = $effectiveStatus recordedStatus = $run.status workerAlive = $alive workerPid = if ($run.PSObject.Properties['workerPid']) { $run.workerPid } else { $null } mode = $run.mode agent = $run.agent repoPath = $run.repoPath branch = $run.branch branchExists = $branchExists startedAt = $run.startedAt endedAt = if ($run.PSObject.Properties['endedAt']) { $run.endedAt } else { $null } elapsed = $elapsed gnhfExitCode = if ($run.PSObject.Properties['gnhfExitCode']) { $run.gnhfExitCode } else { $null } commitCount = $commits.Count commits = $commits diffStat = $diffStat stopWhen = if ($run.PSObject.Properties['stopWhen']) { $run.stopWhen } else { $null } endState = $endState verifyCommand = if ($run.PSObject.Properties['verifyCommand']) { $run.verifyCommand } else { $null } verifyExitCodeBefore = if ($run.PSObject.Properties['verifyExitCodeBefore']) { $run.verifyExitCodeBefore } else { $null } verifyExitCodeAfter = if ($run.PSObject.Properties['verifyExitCodeAfter']) { $run.verifyExitCodeAfter } else { $null } runDir = $runDir gnhfKind = if ($run.PSObject.Properties['gnhfKind']) { $run.gnhfKind } else { $null } gnhfVersion = if ($run.PSObject.Properties['gnhfVersion']) { $run.gnhfVersion } else { $null } vaultNote = $vaultNote notesTail = $notesTailText prompt = $run.prompt } } if ($Json) { $arr = @($results) # NOTE: do not name this $json -- PowerShell variables are case-insensitive, so it would # assign a string to the [switch]$Json parameter and throw at the call site. $jsonText = $arr | ConvertTo-Json -Depth 10 if ($arr.Count -eq 1) { "[$jsonText]" } else { $jsonText } return } foreach ($r in $results) { Write-Host '' Write-Host "=== $($r.id) ===" -ForegroundColor Cyan Write-Host " status : $($r.status)$(if ($r.status -eq 'orphaned') { ' <- worker died without finalizing' })" Write-Host " repo/branch : $($r.repoPath) [$($r.branch)$(if (-not $r.branchExists) { ' MISSING' })]" Write-Host " agent/mode : $($r.agent) / $($r.mode)" Write-Host " started : $($r.startedAt) elapsed: $($r.elapsed)" Write-Host " commits : $($r.commitCount) $($r.diffStat)" if ($r.stopWhen) { Write-Host " stop-when : $($r.stopWhen)" } if ($r.endState) { Write-Host " end-state : $($r.endState.status) (iterations=$($r.endState.iterations))" } if ($r.verifyCommand) { Write-Host " verify : '$($r.verifyCommand)' before=$($r.verifyExitCodeBefore) after=$($r.verifyExitCodeAfter)" } if ($r.vaultNote) { Write-Host " vault note : $($r.vaultNote)" } if ($r.runDir) { Write-Host " run dir : $($r.runDir)" } if ($r.notesTail) { Write-Host " --- notes tail ---" $r.notesTail -split "`n" | ForEach-Object { Write-Host " $_" } } } Write-Host ''