Payload/scripts/kaden/harness/harness-timeline.ps1
|
#Requires -Version 5.1 <# .SYNOPSIS Generate a human-readable journal timeline grouped by turn_id. .DESCRIPTION Reads journal/events.jsonl and writes journal/timeline.md with turns as sections and sensor / phase lines nested under the owning turn_id. .EXAMPLE pwsh -File scripts/kaden/harness-invoke.ps1 harness-timeline.ps1 -Feature saved-search-queries -Slice mo-save-list #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Feature, [string]$Slice = '', [string]$FeatureRoot = '', [string]$RepoRoot = '', [int]$GapMinutes = 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 Test-HarnessContractModuleReady if ([string]::IsNullOrWhiteSpace($RepoRoot)) { $RepoRoot = Get-HarnessRepoRoot -StartPath $PSScriptRoot } $featureRootPath = Get-HarnessFeatureRoot -RepoRoot $RepoRoot -Feature $Feature ` -FeatureRootOverride $FeatureRoot -AllowArbitraryFeatureRoot:$AllowArbitraryFeatureRoot $events = @(Get-HarnessJournalEvents -FeatureRoot $featureRootPath) if ($Slice) { $events = @($events | Where-Object { [string]$_.slice -eq $Slice }) } $turnGroups = [System.Collections.ArrayList]@() $turnIndex = @{} $openTurn = $null foreach ($journalEvent in ($events | Sort-Object { [int](Get-HarnessProperty -Object $_ -Name 'seq' -Default 0) })) { $kind = [string](Get-HarnessProperty -Object $journalEvent -Name 'kind') $refs = Get-HarnessProperty -Object $journalEvent -Name 'refs' $turnId = [string](Get-HarnessProperty -Object $refs -Name 'turn_id') $activity = Get-HarnessProperty -Object $journalEvent -Name 'activity' $activityName = [string](Get-HarnessProperty -Object $activity -Name 'name') if ($kind -eq 'ActivityStarted' -and $activityName -eq 'start') { $turnId = [string](Get-HarnessProperty -Object $activity -Name 'id') $openTurn = [pscustomobject]@{ turn_id = $turnId slice = [string]$journalEvent.slice started_at = [string]$journalEvent.occurred_at stopped_at = '' summary = [string](Get-HarnessProperty -Object $journalEvent -Name 'summary') events = [System.Collections.ArrayList]@() } [void]$turnGroups.Add($openTurn) $turnIndex[$turnId] = $openTurn continue } if ($kind -eq 'ActivityStopped' -and $activityName -eq 'stop' -and $openTurn) { $openTurn.stopped_at = [string]$journalEvent.occurred_at [void]$openTurn.events.Add($journalEvent) $openTurn = $null continue } $target = $null if (-not [string]::IsNullOrWhiteSpace($turnId) -and $turnIndex.ContainsKey($turnId)) { $target = $turnIndex[$turnId] } elseif ($openTurn) { $target = $openTurn } if ($target) { [void]$target.events.Add($journalEvent) } else { $orphanId = "orphan-$([string]$journalEvent.seq)" $orphan = [pscustomobject]@{ turn_id = $orphanId slice = [string]$journalEvent.slice started_at = [string]$journalEvent.occurred_at stopped_at = '' summary = 'events outside a turn window' events = [System.Collections.ArrayList]@(@($journalEvent)) } [void]$turnGroups.Add($orphan) $turnIndex[$orphanId] = $orphan } } function Format-HarnessTimelineEventLine { param([Parameter(Mandatory = $true)]$JournalEvent) $kind = [string](Get-HarnessProperty -Object $JournalEvent -Name 'kind') $outcome = [string](Get-HarnessProperty -Object $JournalEvent -Name 'outcome') $payload = Get-HarnessProperty -Object $JournalEvent -Name 'payload' $actor = Get-HarnessProperty -Object $JournalEvent -Name 'actor' $agent = [string](Get-HarnessProperty -Object $actor -Name 'agent') $occurredRaw = Get-HarnessProperty -Object $JournalEvent -Name 'occurred_at' $occurred = if ($null -ne $occurredRaw) { Convert-HarnessTimestampToUtcZString -Value $occurredRaw } else { '' } $payloadName = [string](Get-HarnessProperty -Object $payload -Name 'name') $line = "- $occurred $kind" if ($kind -eq 'SensorRunRecorded' -and $payloadName) { $line += " sensor=$payloadName" if ($outcome) { $line += " outcome=$outcome" } } elseif ($kind -eq 'PhaseStatusChanged') { $phase = [string](Get-HarnessProperty -Object $payload -Name 'phase') $status = [string](Get-HarnessProperty -Object $payload -Name 'status') if ($phase) { $line += " phase=$phase status=$status" } } elseif ($kind -in @('ActivityStarted', 'ActivityStopped')) { if ($agent) { $line += " agent=$agent" } } $summary = [string](Get-HarnessProperty -Object $JournalEvent -Name 'summary') if ($summary) { $line += " -- $summary" } return $line } $lines = [System.Collections.ArrayList]@() $titleSlice = if ($Slice) { " / $Slice" } else { '' } [void]$lines.Add("# Journal timeline -- $Feature$titleSlice") [void]$lines.Add('') [void]$lines.Add("Generated: $(Get-HarnessIso8601UtcNow)") [void]$lines.Add('') $gapThreshold = if ($GapMinutes -gt 0) { $GapMinutes } else { 8 } foreach ($turn in $turnGroups) { $window = $turn.started_at if ($turn.stopped_at) { $window += " -> $($turn.stopped_at)" } [void]$lines.Add("## Turn $($turn.turn_id) (slice $($turn.slice))") [void]$lines.Add($window) if ($turn.summary) { [void]$lines.Add($turn.summary) } [void]$lines.Add('') $openPhases = @{} $prevInstant = $null $startRaw = [string]$turn.started_at if (-not [string]::IsNullOrWhiteSpace($startRaw)) { try { $prevInstant = [datetime]::Parse($startRaw, $null, [System.Globalization.DateTimeStyles]::AdjustToUniversal) } catch { $prevInstant = $null } } foreach ($ev in $turn.events) { $evInstant = Convert-HarnessJournalEventInstant -Event $ev -PreferOccurredAt if ($null -ne $prevInstant -and $null -ne $evInstant) { $gapMin = [Math]::Floor(($evInstant - $prevInstant).TotalMinutes) if ($gapMin -gt $gapThreshold) { [void]$lines.Add("- gap $gapMin min") } } [void]$lines.Add((Format-HarnessTimelineEventLine -JournalEvent $ev)) $kind = [string](Get-HarnessProperty -Object $ev -Name 'kind') if ($kind -eq 'PhaseStatusChanged') { $payload = Get-HarnessProperty -Object $ev -Name 'payload' $phase = [string](Get-HarnessProperty -Object $payload -Name 'phase') $status = [string](Get-HarnessProperty -Object $payload -Name 'status') $occurredRaw = Get-HarnessProperty -Object $ev -Name 'occurred_at' if ($null -eq $occurredRaw -or [string]::IsNullOrWhiteSpace([string]$occurredRaw)) { $occurredRaw = Get-HarnessProperty -Object $ev -Name 'recorded_at' } $occurred = if ($null -ne $occurredRaw) { Convert-HarnessTimestampToUtcZString -Value $occurredRaw } else { '' } if (-not [string]::IsNullOrWhiteSpace($phase)) { if ($status -eq 'in_progress') { $openPhases[$phase] = $occurred } elseif ($status -eq 'complete' -or $status -eq 'skipped') { $openPhases.Remove($phase) } } } if ($null -ne $evInstant) { $prevInstant = $evInstant } } foreach ($phaseName in @($openPhases.Keys | Sort-Object)) { [void]$lines.Add("- open phase=$phaseName since $($openPhases[$phaseName])") } [void]$lines.Add('') } $journalDir = Get-HarnessJournalDirectory -FeatureRoot $featureRootPath if (-not (Test-Path -LiteralPath $journalDir)) { New-Item -ItemType Directory -Path $journalDir -Force | Out-Null } $outPath = Join-Path $journalDir 'timeline.md' Set-Content -LiteralPath $outPath -Value ($lines -join [Environment]::NewLine) -Encoding UTF8 Write-Output "harness-timeline: wrote $outPath ($($turnGroups.Count) turn section(s), $($events.Count) event(s))" |