<# .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 engine 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 -or ($_.PSObject.Properties.Name -contains 'registryId' -and $_.registryId -eq $Id) -or ($_.PSObject.Properties.Name -contains 'launchId' -and $_.launchId -eq $Id) -or ($_.PSObject.Properties.Name -contains 'engineRunId' -and $_.engineRunId -eq $Id) }) $launchMatches = @($runs | Where-Object { $_.PSObject.Properties.Name -contains 'launchId' -and $_.launchId -eq $Id } | Sort-Object { [datetime]::Parse($_.startedAt) } -Descending) if ($launchMatches.Count -gt 0) { $runs = @($launchMatches | Select-Object -First 1) } } 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 } $checkoutPath = Resolve-LightsOutCheckoutPath -Run $run $runDir = $null if ($checkoutPath -and (Test-Path -LiteralPath $checkoutPath)) { $runDir = Resolve-LightsOutRunDir -RepoPath $checkoutPath -RunId $run.id } $endState = $null if ($runDir) { $endStatePath = Join-Path $runDir 'end-state.json' if (Test-Path -LiteralPath $endStatePath) { try { $endState = Get-Content -LiteralPath $endStatePath -Raw | ConvertFrom-Json } catch { } } } $commits = @() $diffStat = '' $branchExists = $false $canReadDiff = $checkoutPath -and (Test-Path -LiteralPath $checkoutPath) if (($run.PSObject.Properties['worktree'] -and $run.worktree) -and -not $runDir) { $canReadDiff = $false } if ($canReadDiff -and $run.baseCommit) { $branchExists = [bool](git -C $checkoutPath rev-parse --verify --quiet $run.branch 2>$null) $ref = if ($branchExists) { $run.branch } else { 'HEAD' } $commits = @(git -C $checkoutPath --no-pager log --oneline "$($run.baseCommit)..$ref" 2>$null) $diffStat = (git -C $checkoutPath --no-pager diff --shortstat "$($run.baseCommit)..$ref" 2>$null) -join ' ' } $notesTailText = $null if ($runDir) { $notesPath = Join-Path $runDir 'notes.md' if (Test-Path -LiteralPath $notesPath) { $notesTailText = (Get-Content -LiteralPath $notesPath -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 = Get-RunPropertyValue -InputObject $run -Names @('vaultNote') if (-not $vaultNote) { $guess = Join-Path (Get-LightsOutVaultFolder -Run $run) 'summary.md' if (Test-Path -LiteralPath $guess) { $vaultNote = $guess } } $effectiveStatus = $run.status if ($run.status -in @('running', 'launching', 'finalizing') -and -not $alive) { $hasWorkerPid = $run.PSObject.Properties['workerPid'] -and $run.workerPid $startedRecently = $false try { $startedRecently = ([datetime]::Parse($run.startedAt)) -gt (Get-Date).AddMinutes(-2) } catch { } if ($hasWorkerPid -or -not $startedRecently) { $effectiveStatus = 'orphaned' } } $engine = Get-RunEngineDescriptor -Run $run [pscustomobject]@{ id = $run.id registryId = if ($run.PSObject.Properties['registryId']) { $run.registryId } else { $null } launchId = if ($run.PSObject.Properties['launchId']) { $run.launchId } else { $null } engineRunId = if ($run.PSObject.Properties['engineRunId']) { $run.engineRunId } else { $null } 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 checkoutPath = $checkoutPath branch = $run.branch branchExists = $branchExists startedAt = $run.startedAt endedAt = if ($run.PSObject.Properties['endedAt']) { $run.endedAt } else { $null } elapsed = $elapsed engineExitCode = $engine.ExitCode 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 engineKind = $engine.Kind engineSource = $engine.Source engineVersion = $engine.Version vaultNote = $vaultNote notesTail = $notesTailText prompt = $run.prompt } } if ($Json) { $array = @($results) $jsonText = $array | ConvertTo-Json -Depth 10 if ($array.Count -eq 1) { "[$jsonText]" } else { $jsonText } return } foreach ($result in $results) { Write-Host '' Write-Host "=== $($result.id) ===" -ForegroundColor Cyan Write-Host " status : $($result.status)$(if ($result.status -eq 'orphaned') { ' <- worker died without finalizing' })" Write-Host " repo/branch : $($result.repoPath) [$($result.branch)$(if (-not $result.branchExists) { ' MISSING' })]" Write-Host " agent/mode : $($result.agent) / $($result.mode)" if ($result.engineKind) { $engineLine = $result.engineKind if ($result.engineVersion) { $engineLine += " v$($result.engineVersion)" } if ($null -ne $result.engineExitCode) { $engineLine += " exit=$($result.engineExitCode)" } Write-Host " engine : $engineLine" } Write-Host " started : $($result.startedAt) elapsed: $($result.elapsed)" Write-Host " commits : $($result.commitCount) $($result.diffStat)" if ($result.stopWhen) { Write-Host " stop-when : $($result.stopWhen)" } if ($result.endState) { Write-Host " end-state : $($result.endState.status) (iterations=$($result.endState.iterations))" } if ($result.verifyCommand) { Write-Host " verify : '$($result.verifyCommand)' before=$($result.verifyExitCodeBefore) after=$($result.verifyExitCodeAfter)" } if ($result.vaultNote) { Write-Host " vault note : $($result.vaultNote)" } if ($result.runDir) { Write-Host " run dir : $($result.runDir)" } if ($result.notesTail) { Write-Host " --- notes tail ---" $result.notesTail -split "`n" | ForEach-Object { Write-Host " $_" } } } Write-Host ''