Payload/scripts/kaden/harness/harness-record-handoff.ps1
|
#Requires -Version 5.1 <# .SYNOPSIS Record a handoff artifact as journal truth and rebuild projection. .DESCRIPTION Validates the markdown handoff front matter before it is written, writes the handoff artifact, appends HandoffRecorded to the journal, and rebuilds the gate projection. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Feature, [Parameter(Mandatory = $true)] [string]$Slice, [Parameter(Mandatory = $true)] [string]$From, [Parameter(Mandatory = $true)] [string]$To, [Parameter(Mandatory = $true)] [string]$PayloadPath, [string]$HandoffArtifact = '', [string]$FeatureRoot = '', [string]$RepoRoot = '', [ValidateSet('cursor', 'copilot', 'other')] [string]$ModelProvider = 'copilot', [string]$ModelSlug = 'unknown', [ValidateSet('primary', 'subagent')] [string]$ModelRole = 'primary', [string]$Agent = 'atdd-developer', [string]$ParentAgent = '', [ValidateSet('cursor-task', 'copilot-agent', 'chat', 'harness-script')] [string]$Tool = 'harness-script', [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-handoff' | Out-Null $featureRootPath = Get-HarnessFeatureRoot -RepoRoot $RepoRoot -Feature $Feature -FeatureRootOverride $FeatureRoot -AllowArbitraryFeatureRoot:$AllowArbitraryFeatureRoot if (-not (Test-Path -LiteralPath $PayloadPath)) { throw "Payload not found: $PayloadPath" } $payloadText = Get-Content -LiteralPath $PayloadPath -Raw if ([string]::IsNullOrWhiteSpace($payloadText)) { throw 'Handoff payload is empty.' } if ($HandoffArtifact) { $relativeArtifact = $HandoffArtifact } else { $relativeArtifact = "handoff-$Slice.md" } if ($payloadText -notmatch '(?ms)^---\s*\r?\n.*?\r?\n---\s*\r?\n') { throw 'Handoff payload must start with YAML front matter.' } foreach ($requiredKey in @('handoff_version', 'from', 'to', 'feature', 'slice', 'ownership_status', 'status', 'timestamp')) { if (-not (Test-HarnessYamlBlockPresent -Content $payloadText -Key $requiredKey)) { throw "Handoff payload missing required '${requiredKey}:' front matter" } } if ((Get-HarnessYamlScalarMatch -Content $payloadText -Key 'from') -ne $From) { throw "Handoff payload from must be '$From'." } if ((Get-HarnessYamlScalarMatch -Content $payloadText -Key 'to') -ne $To) { throw "Handoff payload to must be '$To'." } if ((Get-HarnessYamlScalarMatch -Content $payloadText -Key 'slice') -ne $Slice) { throw "Handoff payload slice must be '$Slice'." } if ((Get-HarnessYamlScalarMatch -Content $payloadText -Key 'feature') -ne $Feature) { throw "Handoff payload feature must be '$Feature'." } if ((Get-HarnessYamlScalarMatch -Content $payloadText -Key 'status') -ne 'complete') { throw 'Handoff payload status must be complete.' } $handoffOwnership = Get-HarnessYamlScalarMatch -Content $payloadText -Key 'ownership_status' if ($handoffOwnership -eq 'review_ready') { throw "Handoff payload must not claim ownership_status: review_ready (elevation is GateValidationPassed only). Use in_progress or blocked." } if (-not (Test-HarnessUtcTimestamp -Value (Get-HarnessYamlScalarMatch -Content $payloadText -Key 'timestamp'))) { throw 'Handoff payload timestamp must be RFC 3339 UTC Z.' } $fullArtifactPath = Resolve-HarnessPathUnderFeatureRoot -FeatureRoot $featureRootPath -RelativePath $relativeArtifact -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 $invocation = Format-HarnessModelInvocation -Agent $Agent -Provider $ModelProvider -Slug $resolvedSlug -Role $ModelRole ` -ParentAgent $(if ($ParentAgent) { $ParentAgent } else { $null }) -Tool $Tool $actor = New-HarnessJournalActor -Agent $Agent -ParentAgent $(if ($ParentAgent) { $ParentAgent } else { $null }) ` -Tool $Tool -Provider $ModelProvider -Slug $resolvedSlug -Role $ModelRole $activity = New-HarnessJournalActivity -Id (New-HarnessJournalActivityId) -Type 'handoff' -Name $Slice -Role 'instant' $journalEvent = Add-HarnessJournalEvent -FeatureRoot $featureRootPath -Feature $Feature -Slice $Slice ` -Kind 'HandoffRecorded' -Activity $activity -Actor $actor -Outcome 'PASS' ` -Summary "handoff recorded for $Slice" ` -Payload ([pscustomobject]@{ handoff = [pscustomobject]@{ from = $From to = $To artifact = $relativeArtifact status = 'complete' timestamp = Get-HarnessYamlScalarMatch -Content $payloadText -Key 'timestamp' invocation = $invocation } }) ` -Refs ([pscustomobject]@{ amends_event_id = $null pair_event_id = $null artifact = $relativeArtifact command = '.\scripts\kaden\harness-record-handoff.ps1' supersedes_event_id = $null }) Update-HarnessGateProjectionFromJournal -FeatureRoot $featureRootPath -Feature $Feature -RepoRoot $RepoRoot -Write | Out-Null Remove-HarnessStagedPayloadIfEligible -CandidatePath $PayloadPath -RepoRoot $RepoRoot -FeatureRoot $featureRootPath [pscustomobject]@{ operation = 'record-handoff' feature = $Feature slice = $Slice artifact = $relativeArtifact canonical_path = $fullArtifactPath journal_event_ids = @($journalEvent.event_id) status = 'PASS' } | ConvertTo-Json -Depth 10 |