Payload/scripts/harness/harness-record-slice-verdict.ps1
|
#Requires -Version 5.1 <# .SYNOPSIS Record a slice-review verdict artifact as journal truth and rebuild projection. .DESCRIPTION Validates the slice-verdict YAML payload before it is written, writes the review artifact, and then records the slice-review feature phase through the existing harness-record-phase.ps1 path so the projection is rebuilt from journal truth. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Feature, [Parameter(Mandatory = $true)] [string]$Slice, [Parameter(Mandatory = $true)] [string]$PayloadPath, [Parameter(Mandatory = $true)] [string]$VerdictArtifact, # Raw Task/subagent slice-reviewer YAML. Must exist under the feature root before recording. [Parameter(Mandatory = $true)] [string]$SourceArtifact, # Optional path to slice-review-<slice>-<date>.md; when omitted, validation still requires one on disk. [string]$NarrativeArtifact = '', [string]$FeatureRoot = '', [string]$RepoRoot = '', [ValidateSet('cursor', 'copilot', 'other')] [string]$ModelProvider = 'copilot', [string]$ModelSlug = 'unknown', [ValidateSet('primary', 'subagent')] [string]$ModelRole = 'primary', [string]$Agent = 'slice-reviewer', [string]$ParentAgent = '', [ValidateSet('cursor-task', 'copilot-agent', 'chat', 'harness-script')] [string]$Tool = 'harness-script', [string]$SourceInvocationId = '', [ValidateSet('host-id', 'artifact-attested')] [string]$SourceProvenance = 'host-id', [switch]$AllowStub, [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 Import-Module (Join-Path $PSScriptRoot 'lib\RepoRuntime.psm1') -Force Test-HarnessContractModuleReady if ([string]::IsNullOrWhiteSpace($RepoRoot)) { $RepoRoot = Get-HarnessRepoRoot -StartPath $PSScriptRoot } Initialize-RepoShellEnvironment -LogPrefix 'harness-record-slice-verdict' | Out-Null $featureRootPath = Get-HarnessFeatureRoot -RepoRoot $RepoRoot -Feature $Feature -FeatureRootOverride $FeatureRoot -AllowArbitraryFeatureRoot:$AllowArbitraryFeatureRoot if ($SourceProvenance -eq 'host-id' -and [string]::IsNullOrWhiteSpace($SourceInvocationId)) { throw 'slice-review with SourceProvenance host-id requires -SourceInvocationId.' } if ($SourceProvenance -eq 'artifact-attested' -and ($ModelProvider -ne 'copilot' -or -not [string]::IsNullOrWhiteSpace($SourceInvocationId))) { throw 'SourceProvenance artifact-attested is only valid for Copilot subagents without a host invocation id.' } Assert-HarnessPipelineSourceNotScratch -Path $NarrativeArtifact -RepoRoot $RepoRoot if (-not (Test-Path -LiteralPath $PayloadPath)) { throw "Payload not found: $PayloadPath" } $payloadText = Get-Content -LiteralPath $PayloadPath -Raw if ([string]::IsNullOrWhiteSpace($payloadText)) { throw 'Slice verdict payload is empty.' } Assert-HarnessSliceVerdictQualityGatesComplete -RepoRoot $RepoRoot -FeatureRoot $featureRootPath $stampUtc = Get-HarnessIso8601UtcNow $payloadText = Format-HarnessYamlTimestampField -Content $payloadText -Timestamp $stampUtc # Raw source must exist (and live under .reviews/) before the canonical verdict is written. $sourceFull = Resolve-HarnessPathUnderFeatureRoot -FeatureRoot $featureRootPath -RelativePath $SourceArtifact -ThrowOnEscape if (-not (Test-Path -LiteralPath $sourceFull)) { throw "SourceArtifact not found: $SourceArtifact (save the slice-reviewer Task output verbatim under .reviews/ first)" } $sourceLeaf = Split-Path $sourceFull -Leaf if ($sourceLeaf -notlike 'slice-reviewer-*') { throw "SourceArtifact leaf name must start with 'slice-reviewer-' (found '$sourceLeaf')" } $reviewsDir = Join-Path $featureRootPath '.reviews' if (-not (Test-Path -LiteralPath $reviewsDir)) { New-Item -ItemType Directory -Path $reviewsDir -Force | Out-Null } $sourceUnderReviews = Join-Path $reviewsDir $sourceLeaf if ($sourceFull -ne $sourceUnderReviews) { Copy-Item -LiteralPath $sourceFull -Destination $sourceUnderReviews -Force } if (-not [string]::IsNullOrWhiteSpace($NarrativeArtifact)) { $narrativeFull = Resolve-HarnessPathUnderFeatureRoot -FeatureRoot $featureRootPath -RelativePath $NarrativeArtifact -ThrowOnEscape if (-not (Test-Path -LiteralPath $narrativeFull)) { throw "NarrativeArtifact not found: $NarrativeArtifact" } $narrativeLeaf = Split-Path $narrativeFull -Leaf if ($narrativeLeaf -notmatch '^slice-review-') { throw "NarrativeArtifact leaf name must start with 'slice-review-' (found '$narrativeLeaf')" } $narrativeDest = Join-Path $featureRootPath $narrativeLeaf if ($narrativeFull -ne $narrativeDest) { Copy-Item -LiteralPath $narrativeFull -Destination $narrativeDest -Force } } elseif (-not (Find-HarnessSliceReviewNarrative -FeatureRoot $featureRootPath -SliceId $Slice)) { throw "Missing slice-review-$Slice-<YYYY-MM-DD>.md under feature root (or pass -NarrativeArtifact). YAML alone is not durable." } Assert-HarnessSliceReviewAllowlistOnDisk -FeatureRoot $featureRootPath -SliceId $Slice -VerdictArtifact $VerdictArtifact -AllowStub:$AllowStub $tempRoot = Join-Path ([System.IO.Path]::GetTempPath()) ('harness-slice-verdict-validate-' + [guid]::NewGuid().ToString('N')) $tempArtifact = Resolve-HarnessPathUnderFeatureRoot -FeatureRoot $tempRoot -RelativePath $VerdictArtifact -ThrowOnEscape New-Item -ItemType Directory -Path (Split-Path $tempArtifact -Parent) -Force | Out-Null [System.IO.File]::WriteAllText($tempArtifact, $payloadText, [System.Text.UTF8Encoding]::new($false)) # Temp root also needs the raw source + narrative so validation mirrors the real feature root. $tempReviews = Join-Path $tempRoot '.reviews' New-Item -ItemType Directory -Path $tempReviews -Force | Out-Null Copy-Item -LiteralPath $sourceUnderReviews -Destination (Join-Path $tempReviews $sourceLeaf) -Force $narrativeOnDisk = Find-HarnessSliceReviewNarrative -FeatureRoot $featureRootPath -SliceId $Slice if ($narrativeOnDisk) { Copy-Item -LiteralPath $narrativeOnDisk.FullName -Destination (Join-Path $tempRoot $narrativeOnDisk.Name) -Force } elseif (-not [string]::IsNullOrWhiteSpace($NarrativeArtifact)) { $nFull = Resolve-HarnessPathUnderFeatureRoot -FeatureRoot $featureRootPath -RelativePath $NarrativeArtifact -ThrowOnEscape Copy-Item -LiteralPath $nFull -Destination (Join-Path $tempRoot (Split-Path $nFull -Leaf)) -Force } try { $findings = @(Invoke-HarnessSliceReviewVerdictValidation -FeatureRoot $tempRoot -SliceId $Slice -NotBeforeUtc '') if ($findings.Count -gt 0) { $messages = @($findings | ForEach-Object { $_.message }) throw "Slice verdict payload failed validation: $($messages -join '; ')" } } finally { Remove-Item -LiteralPath $tempRoot -Recurse -Force -ErrorAction SilentlyContinue } $fullArtifactPath = Resolve-HarnessPathUnderFeatureRoot -FeatureRoot $featureRootPath -RelativePath $VerdictArtifact -ThrowOnEscape New-Item -ItemType Directory -Path (Split-Path $fullArtifactPath -Parent) -Force | Out-Null [System.IO.File]::WriteAllText($fullArtifactPath, $payloadText, [System.Text.UTF8Encoding]::new($false)) $resolvedSlug = Get-HarnessResolvedModelSlug -ModelSlug $ModelSlug $actor = New-HarnessJournalActor -Agent $Agent -ParentAgent $(if ($ParentAgent) { $ParentAgent } else { $null }) ` -Tool $Tool -Provider $ModelProvider -Slug $resolvedSlug -Role $ModelRole $activityId = New-HarnessJournalActivityId $activity = New-HarnessJournalActivity -Id $activityId -Type 'pipeline.phase' -Name 'slice-review' -Role 'instant' Add-HarnessJournalEvent -FeatureRoot $featureRootPath -Feature $Feature -Slice $Slice ` -Kind 'PhaseStatusChanged' -Activity $activity -Actor $actor -Outcome $null ` -Summary 'feature phase slice-review status=complete' ` -Payload ([pscustomobject]@{ feature_phase = 'slice-review' status = 'complete' artifacts = @($VerdictArtifact) source_provenance = $SourceProvenance source_invocation_id = $SourceInvocationId }) ` -Refs ([pscustomobject]@{ amends_event_id = $null pair_event_id = $null artifact = $VerdictArtifact command = '.\scripts\harness-record-slice-verdict.ps1' }) | Out-Null Update-HarnessGateProjectionFromJournal -FeatureRoot $featureRootPath -Feature $Feature -RepoRoot $RepoRoot -Write | Out-Null Sync-HarnessOwnSliceNextArtifact -FeatureRoot $featureRootPath -Feature $Feature -Slice $Slice -RepoRoot $RepoRoot | Out-Null Remove-HarnessStagedPayloadIfEligible -CandidatePath $PayloadPath -RepoRoot $RepoRoot -FeatureRoot $featureRootPath Remove-HarnessStagedPayloadIfEligible -CandidatePath $sourceFull -RepoRoot $RepoRoot -FeatureRoot $featureRootPath [pscustomobject]@{ operation = 'record-slice-verdict' feature = $Feature slice = $Slice artifact = $VerdictArtifact canonical_path = $fullArtifactPath status = 'PASS' } | ConvertTo-Json -Depth 10 |