Payload/scripts/kaden/utils/journal-repair.ps1
|
<# .SYNOPSIS Append a journal correction or a new journal event, then rebuild the gate projection. .DESCRIPTION Operator recovery for a journal the agent must not hand-edit. Existing lines stay as they are. adjust appends ActivityAdjusted against an event id. append adds one new allowed kind. Both rebuild .review-loop.json from the journal. ActivityAdjusted is ledger-only. It does not clear a gate. This script also refuses HumanGateDecided (use scripts\kaden\utils\record-slice-approved.ps1), GateValidationPassed (that status comes from validate-review-loop), and turn boundaries (ActivityStarted, ActivityStopped, and activity names start or stop). Nothing is written unless -Apply is present. -Preview prints the same plan and also writes nothing. The feature folder and slice must already exist. This script refuses to invent a slice. .PARAMETER Feature Feature slug under docs/harness/features. .PARAMETER Slice Existing slice id on that feature's .review-loop.json. .PARAMETER Action adjust or append. .PARAMETER Kind Journal kind for append. Ignored for adjust, which always writes ActivityAdjusted. .PARAMETER AmendsEventId Event id the adjusting entry corrects. Required for adjust. .PARAMETER Summary Journal summary line. .PARAMETER Correction Text stored on the adjusting entry payload. Used by adjust. .PARAMETER Outcome Optional outcome for append. Empty leaves the outcome null. .PARAMETER PayloadPath JSON object file merged as the append payload. .PARAMETER Preview Validate and print the plan. Write nothing. This is also the default when -Apply is omitted. .PARAMETER Apply Append the journal line and rebuild the projection. Required for any write. .EXAMPLE pwsh -NoProfile -File .\scripts\kaden\utils\journal-repair.ps1 -Action adjust -Feature <slug> -Slice <id> -AmendsEventId <event_id> -Summary "Correction: note the real sensor path" -Correction "Operator attached the original path." -Apply .EXAMPLE pwsh -NoProfile -File .\scripts\kaden\utils\journal-repair.ps1 -Action append -Feature <slug> -Slice <id> -Kind BlockerDeclared -Summary "Operator marked the slice blocked" .EXAMPLE Get-Help .\scripts\kaden\utils\journal-repair.ps1 -Parameter Action #> #Requires -Version 5.1 [CmdletBinding(DefaultParameterSetName = "Run")] param( [Parameter(ParameterSetName = "Run")] [string]$Feature = "", [Parameter(ParameterSetName = "Run")] [string]$Slice = "", [Parameter(ParameterSetName = "Run")] [string]$Action = "", [Parameter(ParameterSetName = "Run")] [string]$Kind = "", [Parameter(ParameterSetName = "Run")] [string]$AmendsEventId = "", [Parameter(ParameterSetName = "Run")] [string]$Summary = "", [Parameter(ParameterSetName = "Run")] [string]$Correction = "", [Parameter(ParameterSetName = "Run")] [string]$Outcome = "", [Parameter(ParameterSetName = "Run")] [string]$PayloadPath = "", [Parameter(ParameterSetName = "Run")] [string]$ActivityType = "other", [Parameter(ParameterSetName = "Run")] [string]$ActivityName = "operator-repair", [Parameter(ParameterSetName = "Run")] [string]$RepoRoot = "", [Parameter(ParameterSetName = "Run")] [string]$FeatureRoot = "", [Parameter(ParameterSetName = "Run")] [switch]$AllowArbitraryFeatureRoot, [Parameter(ParameterSetName = "Run")] [string]$Agent = "operator", [Parameter(ParameterSetName = "Run")] [string]$Tool = "journal-repair", [Parameter(ParameterSetName = "Run")] [string]$ModelProvider = "other", [Parameter(ParameterSetName = "Run")] [string]$ModelSlug = "unknown", [Parameter(ParameterSetName = "Run")] [string]$ModelRole = "primary", [Parameter(ParameterSetName = "Run")] [switch]$Preview, [Parameter(ParameterSetName = "Run")] [switch]$Apply, [Parameter(ParameterSetName = "Help")] [Alias("h", "help")] [switch]$ShowHelp ) Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" function Get-JournalRepairKindCatalog { # Keep this list aligned with Add-HarnessJournalEvent, minus kinds this script refuses. return @( "PhaseStatusChanged", "SensorRunRecorded", "GateCheckRun", "GateRelevantValidationRun", "ReviewerCycleRecorded", "HandoffRecorded", "OwnershipChanged", "BlockerDeclared", "EmissionGapMarked", "TaskStarted", "TaskHeartbeat", "TaskTimedOut", "TaskRecovered", "TaskAbandoned", "ArtifactRegistered", "StoryIdsAssigned", "AdvisoryContinuityRecorded" ) } function Get-JournalRepairActivityTypeCatalog { return @( "pipeline.phase", "slice.phase", "review.cycle", "sensor", "validation", "gate", "handoff", "blocker", "artifact", "other" ) } function Resolve-JournalRepairRepoRoot { param([string]$RequestedRoot) if (-not [string]::IsNullOrWhiteSpace($RequestedRoot)) { $resolved = (Resolve-Path -LiteralPath $RequestedRoot).Path $marker = Join-Path $resolved "scripts\kaden\harness-invoke.ps1" if (-not (Test-Path -LiteralPath $marker)) { throw "RepoRoot is not a Kaden checkout (missing scripts\kaden\harness-invoke.ps1): $resolved" } return $resolved } $current = $PSScriptRoot while ($true) { $marker = Join-Path $current "scripts\kaden\harness-invoke.ps1" if (Test-Path -LiteralPath $marker) { return $current } $parent = Split-Path $current -Parent if ([string]::IsNullOrWhiteSpace($parent) -or $parent -eq $current) { throw "Could not locate repository root (scripts\kaden\harness-invoke.ps1) from this script." } $current = $parent } } function Get-JournalRepairProjectionSlice { param( [Parameter(Mandatory = $true)]$Loop, [Parameter(Mandatory = $true)][string]$SliceId ) foreach ($phase in @($Loop.phases)) { if ($null -eq $phase) { continue } $slices = $phase.PSObject.Properties["slices"] if ($null -eq $slices) { continue } foreach ($candidate in @($slices.Value)) { if ($null -eq $candidate) { continue } $idProp = $candidate.PSObject.Properties["id"] if ($null -ne $idProp -and [string]$idProp.Value -eq $SliceId) { return $candidate } } } return $null } function Resolve-JournalRepairPayloadPath { param( [Parameter(Mandatory = $true)][string]$InputPath, [Parameter(Mandatory = $true)][string]$Root ) if (Test-Path -LiteralPath $InputPath) { return (Resolve-Path -LiteralPath $InputPath).Path } if (-not [System.IO.Path]::IsPathRooted($InputPath)) { $candidate = Join-Path $Root $InputPath if (Test-Path -LiteralPath $candidate) { return (Resolve-Path -LiteralPath $candidate).Path } } return $null } $cliKitManifest = Join-Path $PSScriptRoot "PsCliUiKit\PsCliUiKit.psd1" if (-not (Test-Path -LiteralPath $cliKitManifest)) { throw "PsCliUiKit manifest not found: $cliKitManifest" } Import-Module -Name $cliKitManifest -Force $exitCode = 0 try { if ($ShowHelp) { (Get-Help -Name $PSCommandPath -Full | Out-String -Width 100) | Out-Host } else { $allowedActions = @("adjust", "append") if ($allowedActions -notcontains $Action) { throw "Action must be adjust or append. Get-Help `"$PSCommandPath`" -Parameter Action" } if ([string]::IsNullOrWhiteSpace($Feature)) { throw "Feature is required. Get-Help `"$PSCommandPath`" -Parameter Feature" } if ($Feature -notmatch "^[A-Za-z0-9][A-Za-z0-9_-]*$") { throw "Feature must be a slug of letters, digits, underscore, or hyphen. Get-Help `"$PSCommandPath`" -Parameter Feature" } if ([string]::IsNullOrWhiteSpace($Slice)) { throw "Slice is required. This script will not invent one. Get-Help `"$PSCommandPath`" -Parameter Slice" } if ($Slice -notmatch "^[A-Za-z0-9][A-Za-z0-9_-]*$") { throw "Slice must be a slug of letters, digits, underscore, or hyphen. Get-Help `"$PSCommandPath`" -Parameter Slice" } if ([string]::IsNullOrWhiteSpace($Summary)) { throw "Summary is required. Get-Help `"$PSCommandPath`" -Parameter Summary" } $activityTypes = @(Get-JournalRepairActivityTypeCatalog) if ($activityTypes -notcontains $ActivityType) { throw "Unknown activity type '$ActivityType'. This script does not open or close a turn. Get-Help `"$PSCommandPath`" -Parameter ActivityType" } if ($ActivityName -in @("start", "stop")) { throw "start and stop are turn boundaries. Use harness-turn-start.ps1 or harness-turn-stop.ps1. Get-Help `"$PSCommandPath`" -Parameter ActivityName" } if ($Action -eq "adjust" -and [string]::IsNullOrWhiteSpace($AmendsEventId)) { throw "adjust needs the event id being corrected. Get-Help `"$PSCommandPath`" -Parameter AmendsEventId" } $kindCatalog = @(Get-JournalRepairKindCatalog) if ($Action -eq "append") { if ([string]::IsNullOrWhiteSpace($Kind)) { throw "append needs a journal kind. Get-Help `"$PSCommandPath`" -Parameter Kind" } elseif ($Kind -eq "HumanGateDecided") { throw "Use scripts\kaden\utils\record-slice-approved.ps1 for an operator approval. Get-Help `"$PSCommandPath`" -Parameter Kind" } elseif ($Kind -eq "GateValidationPassed") { throw "review_ready comes from validate-review-loop. This script will not write that kind. Get-Help `"$PSCommandPath`" -Parameter Kind" } elseif ($Kind -eq "ActivityAdjusted") { throw "ActivityAdjusted belongs on -Action adjust. Get-Help `"$PSCommandPath`" -Parameter Action" } elseif ($Kind -eq "ActivityStarted" -or $Kind -eq "ActivityStopped") { throw "ActivityStarted and ActivityStopped are turn boundaries. Use harness-turn-start.ps1 or harness-turn-stop.ps1. Get-Help `"$PSCommandPath`" -Parameter Kind" } elseif ($kindCatalog -notcontains $Kind) { throw "Unknown journal kind '$Kind'. Get-Help `"$PSCommandPath`" -Parameter Kind" } } $allowedOutcomes = @("PASS", "FAIL", "BLOCKED", "SKIPPED", "PASS_WITH_FINDINGS", "DENY") if (-not [string]::IsNullOrWhiteSpace($Outcome) -and $allowedOutcomes -notcontains $Outcome) { throw "Unknown outcome '$Outcome'. Get-Help `"$PSCommandPath`" -Parameter Outcome" } $root = Resolve-JournalRepairRepoRoot -RequestedRoot $RepoRoot Import-Module (Join-Path $root "scripts\kaden\harness\lib\HarnessContract.psm1") -Force Import-Module (Join-Path $root "scripts\kaden\harness\lib\HarnessJournal.psm1") -Force Import-Module (Join-Path $root "scripts\kaden\harness\lib\HarnessGateProjection.psm1") -Force $featureRootPath = Get-HarnessFeatureRoot -RepoRoot $root -Feature $Feature -FeatureRootOverride $FeatureRoot -AllowArbitraryFeatureRoot:$AllowArbitraryFeatureRoot $loopPath = Join-Path $featureRootPath ".review-loop.json" if (-not (Test-Path -LiteralPath $loopPath)) { throw "Projection not found: $loopPath" } $loop = Get-Content -LiteralPath $loopPath -Raw -Encoding utf8 | ConvertFrom-Json $projectionSlice = Get-JournalRepairProjectionSlice -Loop $loop -SliceId $Slice if ($null -eq $projectionSlice) { throw "Slice '$Slice' is not on the projection for feature '$Feature'. Refusing to invent it. Get-Help `"$PSCommandPath`" -Parameter Slice" } $payload = $null if (-not [string]::IsNullOrWhiteSpace($PayloadPath)) { $payloadFull = Resolve-JournalRepairPayloadPath -InputPath $PayloadPath -Root $root if ($null -eq $payloadFull) { throw "Payload file not found: $PayloadPath. Get-Help `"$PSCommandPath`" -Parameter PayloadPath" } $payload = Get-Content -LiteralPath $payloadFull -Raw -Encoding utf8 | ConvertFrom-Json if ($payload -isnot [System.Management.Automation.PSCustomObject]) { throw "Payload JSON must be an object. Get-Help `"$PSCommandPath`" -Parameter PayloadPath" } } if ($Action -eq "adjust") { $events = @(Get-HarnessJournalEvents -FeatureRoot $featureRootPath) $amended = $null foreach ($existing in $events) { if ([string]$existing.event_id -eq $AmendsEventId) { $amended = $existing break } } if ($null -eq $amended) { throw "That event id is not in this feature journal. Get-Help `"$PSCommandPath`" -Parameter AmendsEventId" } } $actor = New-HarnessJournalActor -Agent $Agent -Tool $Tool -Provider $ModelProvider -Slug $ModelSlug -Role $ModelRole $role = if ($Action -eq "adjust") { "adjust" } else { "instant" } $activity = New-HarnessJournalActivity -Id (New-HarnessJournalActivityId) -Type $ActivityType -Name $ActivityName -Role $role $outcomeValue = if ([string]::IsNullOrWhiteSpace($Outcome)) { $null } else { $Outcome } Write-CliSection -Title "Journal repair" -Style "info" Write-CliKeyValue -Label "action" -Value $Action -LabelWidth 12 Write-CliKeyValue -Label "feature" -Value $Feature -LabelWidth 12 Write-CliKeyValue -Label "slice" -Value $Slice -LabelWidth 12 Write-CliKeyValue -Label "summary" -Value $Summary -LabelWidth 12 if ($Action -eq "adjust") { Write-CliKeyValue -Label "amends" -Value $AmendsEventId -LabelWidth 12 } else { Write-CliKeyValue -Label "kind" -Value $Kind -LabelWidth 12 } if ($Preview -or -not $Apply) { $previewMessage = if ($Preview) { "Preview only. Nothing was written." } else { "Nothing was written. Pass -Apply to append." } Write-CliStatus -State "Info" -Message $previewMessage } else { if ($Action -eq "adjust") { $correctionValue = if ([string]::IsNullOrWhiteSpace($Correction)) { $null } else { $Correction } $evt = Add-HarnessJournalAdjustingEntry -FeatureRoot $featureRootPath -Feature $Feature -Slice $Slice ` -AmendsEventId $AmendsEventId -Summary $Summary -Activity $activity -Actor $actor -Correction $correctionValue } else { $evt = Add-HarnessJournalEvent -FeatureRoot $featureRootPath -Feature $Feature -Slice $Slice ` -Kind $Kind -Activity $activity -Actor $actor -Outcome $outcomeValue -Summary $Summary -Payload $payload } Update-HarnessGateProjectionFromJournal -FeatureRoot $featureRootPath -Feature $Feature -RepoRoot $root -Write | Out-Null Write-CliSection -Title "Appended" -Style "success" Write-CliKeyValue -Label "event" -Value "[info]$($evt.event_id)[-]" -LabelWidth 12 Write-CliKeyValue -Label "seq" -Value ([string]$evt.seq) -LabelWidth 12 if ($Action -eq "adjust") { Write-CliColor -Message " [muted]ActivityAdjusted is ledger-only. It does not clear a gate.[-]" } Write-Output $evt.event_id } } } catch { Write-CliSection -Title "Journal repair failed" -Style "error" Write-CliStatus -State "Error" -Message $_.Exception.Message $exitCode = 2 } finally { Write-CliEnd } if ($exitCode -ne 0) { exit $exitCode } |