Payload/scripts/kaden/harness/harness-record-task-step.ps1
|
#Requires -Version 5.1 <# .SYNOPSIS Normalize trusted task output into transactional harness evidence. .DESCRIPTION Consumes a task-output JSON envelope, writes one transactional sensor artifact, appends journal truth, and rebuilds the gate projection from journal state. This avoids manual transcription when terminal/task execution is unstable. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Feature, [Parameter(Mandatory = $true)] [string]$Slice, [Parameter(Mandatory = $true)] [string]$EnvelopePath, [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-task-step' | Out-Null $featureRootPath = Get-HarnessFeatureRoot -RepoRoot $RepoRoot -Feature $Feature -FeatureRootOverride $FeatureRoot -AllowArbitraryFeatureRoot:$AllowArbitraryFeatureRoot if (-not (Test-Path -LiteralPath $EnvelopePath)) { throw "Task envelope not found: $EnvelopePath" } $envelopeRaw = Get-Content -LiteralPath $EnvelopePath -Raw if ([string]::IsNullOrWhiteSpace($envelopeRaw)) { throw 'Task envelope is empty.' } try { $taskEnvelope = ConvertFrom-HarnessJson -Json $envelopeRaw } catch { throw "Task envelope is not valid JSON: $($_.Exception.Message)" } $name = [string](Get-HarnessProperty -Object $taskEnvelope -Name 'name') $phase = [string](Get-HarnessProperty -Object $taskEnvelope -Name 'phase') $expectedExit = Get-HarnessProperty -Object $taskEnvelope -Name 'expected_exit_code' $actualExit = Get-HarnessProperty -Object $taskEnvelope -Name 'exit_code' $command = [string](Get-HarnessProperty -Object $taskEnvelope -Name 'command') if ([string]::IsNullOrWhiteSpace($name)) { throw 'Task envelope missing required field: name' } if ([string]::IsNullOrWhiteSpace($phase)) { throw 'Task envelope missing required field: phase' } if ($phase -notin @('RED', 'GREEN', 'MUTATE', 'REFACTOR', 'COMMIT')) { throw "Task envelope phase must be one of RED/GREEN/MUTATE/REFACTOR/COMMIT (found '$phase')" } if ($null -eq $expectedExit) { throw 'Task envelope missing required field: expected_exit_code' } if ($null -eq $actualExit) { throw 'Task envelope missing required field: exit_code' } if ([string]::IsNullOrWhiteSpace($command)) { throw 'Task envelope missing required field: command' } $expectedExitCode = [int]$expectedExit $actualExitCode = [int]$actualExit $arguments = @((Get-HarnessProperty -Object $taskEnvelope -Name 'arguments')) $testSelector = [string](Get-HarnessProperty -Object $taskEnvelope -Name 'test_selector') $testFiles = @((Get-HarnessProperty -Object $taskEnvelope -Name 'test_files')) $obligationKey = [string](Get-HarnessProperty -Object $taskEnvelope -Name 'obligation_key') $assertionText = [string](Get-HarnessProperty -Object $taskEnvelope -Name 'assertion_text') $evidenceName = [string](Get-HarnessProperty -Object $taskEnvelope -Name 'evidence_name') $stdoutTail = [string](Get-HarnessProperty -Object $taskEnvelope -Name 'stdout_tail') $stderrTail = [string](Get-HarnessProperty -Object $taskEnvelope -Name 'stderr_tail') $startedAt = [string](Get-HarnessProperty -Object $taskEnvelope -Name 'started_at') $completedAt = [string](Get-HarnessProperty -Object $taskEnvelope -Name 'completed_at') $providedCommandPath = [string](Get-HarnessProperty -Object $taskEnvelope -Name 'command_path') $providedCommandHash = [string](Get-HarnessProperty -Object $taskEnvelope -Name 'command_hash') if ([string]::IsNullOrWhiteSpace($startedAt)) { $startedAt = Get-HarnessIso8601UtcNow } if ([string]::IsNullOrWhiteSpace($completedAt)) { $completedAt = Get-HarnessIso8601UtcNow } if (-not (Test-HarnessUtcTimestamp -Value $startedAt)) { throw "Task envelope started_at must be RFC 3339 UTC Z (found '$startedAt')" } if (-not (Test-HarnessUtcTimestamp -Value $completedAt)) { throw "Task envelope completed_at must be RFC 3339 UTC Z (found '$completedAt')" } $snapshotTestFiles = @($testFiles | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) }) $fileHashes = [ordered]@{} foreach ($testFile in $snapshotTestFiles) { if (-not (Test-Path -LiteralPath $testFile)) { throw "Task envelope test file not found: $testFile" } $fileHashes[$testFile] = (Get-FileHash -LiteralPath $testFile -Algorithm SHA256).Hash } $resolvedObligationKey = if ([string]::IsNullOrWhiteSpace($obligationKey)) { if ([string]::IsNullOrWhiteSpace($testSelector)) { $null } else { $testSelector } } else { $obligationKey } $resolvedCommandPath = $providedCommandPath $commandHash = $providedCommandHash if ([string]::IsNullOrWhiteSpace($resolvedCommandPath) -and (Test-Path -LiteralPath $command)) { $resolvedCommandPath = (Resolve-Path -LiteralPath $command).Path } if ([string]::IsNullOrWhiteSpace($commandHash) -and -not [string]::IsNullOrWhiteSpace($resolvedCommandPath) -and (Test-Path -LiteralPath $resolvedCommandPath)) { $commandHash = (Get-FileHash -LiteralPath $resolvedCommandPath -Algorithm SHA256).Hash } $matched = ($actualExitCode -eq $expectedExitCode) $stepVerdict = if ($matched) { 'PASS' } else { 'FAIL' } $summaryText = if ($matched) { "task step $name satisfied expected exit $expectedExitCode" } else { "task step $name expected exit $expectedExitCode but observed $actualExitCode" } $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 $sensorName = if ([string]::IsNullOrWhiteSpace($evidenceName)) { $name } else { $evidenceName } $timestampForFile = Get-HarnessTimestampForFilename $artifactRelativePath = Get-HarnessSensorArtifactRelativePath -Name $sensorName -TimestampForFile $timestampForFile $supersedesEvent = Get-HarnessLatestJournalEventByName -FeatureRoot $featureRootPath -SliceId $Slice -Name $name -Phase $phase $artifactBody = [ordered]@{ sensor_version = '1.0' name = $name type = 'transactional' source = 'harness-record-task-step.ps1' phase = $phase command = $command arguments = @($arguments) expected_exit_code = $expectedExitCode exit_code = $actualExitCode verdict = $stepVerdict started_at = $startedAt completed_at = $completedAt summary = $summaryText stdout_tail = $stdoutTail stderr_tail = $stderrTail invocation = $invocation snapshot = [ordered]@{ test_selector = $(if ([string]::IsNullOrWhiteSpace($testSelector)) { $null } else { $testSelector }) test_files = @($snapshotTestFiles) file_hashes = [pscustomobject]$fileHashes obligation_key = $resolvedObligationKey assertion_text = $(if ([string]::IsNullOrWhiteSpace($assertionText)) { $null } else { $assertionText }) command_path = $resolvedCommandPath command_hash = $commandHash supersedes_event_id = $(if ($supersedesEvent) { $supersedesEvent.event_id } else { $null }) workspace_head = $null workspace_dirty = $true } } Export-HarnessSensorArtifactFile -FeatureRoot $featureRootPath -Payload @{ relative_path = $artifactRelativePath; body = $artifactBody } | Out-Null $journalEventIds = [System.Collections.ArrayList]@() $sensorActivity = New-HarnessJournalActivity -Id (New-HarnessJournalActivityId) -Type 'sensor' -Name $name -Role 'instant' $sensorKind = if ($name -eq 'validate-review-loop') { 'GateCheckRun' } else { 'SensorRunRecorded' } $sensorEvent = Add-HarnessJournalEvent -FeatureRoot $featureRootPath -Feature $Feature -Slice $Slice ` -Kind $sensorKind -Activity $sensorActivity -Actor $actor -Outcome $stepVerdict ` -Summary $summaryText ` -Payload ([pscustomobject]@{ name = $name type = 'transactional' phase = $phase verdict = $stepVerdict evidence_artifact = $artifactRelativePath expected_exit_code = $expectedExitCode exit_code = $actualExitCode command = $command arguments = @($arguments) test_selector = $(if ([string]::IsNullOrWhiteSpace($testSelector)) { $null } else { $testSelector }) test_files = @($snapshotTestFiles) obligation_key = $resolvedObligationKey command_path = $resolvedCommandPath supersedes_event_id = $(if ($supersedesEvent) { $supersedesEvent.event_id } else { $null }) }) ` -Refs ([pscustomobject]@{ amends_event_id = $null pair_event_id = $null artifact = $artifactRelativePath command = '.\\scripts\\harness-record-task-step.ps1' supersedes_event_id = $(if ($supersedesEvent) { $supersedesEvent.event_id } else { $null }) }) [void]$journalEventIds.Add($sensorEvent.event_id) $projectedPhaseStatus = $null if ($matched) { $phaseActivity = New-HarnessJournalActivity -Id (New-HarnessJournalActivityId) -Type 'slice.phase' -Name $phase -Role 'instant' $phaseEvent = Add-HarnessJournalEvent -FeatureRoot $featureRootPath -Feature $Feature -Slice $Slice ` -Kind 'PhaseStatusChanged' -Activity $phaseActivity -Actor $actor -Outcome $null ` -Summary "slice $Slice phase $phase status=complete" ` -Payload ([pscustomobject]@{ phase = $phase status = 'complete' evidence_artifact = $artifactRelativePath }) ` -Refs ([pscustomobject]@{ amends_event_id = $null pair_event_id = $null artifact = $artifactRelativePath command = '.\\scripts\\harness-record-task-step.ps1' }) [void]$journalEventIds.Add($phaseEvent.event_id) $projectedPhaseStatus = 'complete' } Update-HarnessGateProjectionFromJournal -FeatureRoot $featureRootPath -Feature $Feature -RepoRoot $RepoRoot -Write | Out-Null $resultEnvelope = [ordered]@{ operation = $name feature = $Feature slice = $Slice phase = $phase status = $stepVerdict expected_exit_code = $expectedExitCode actual_exit_code = $actualExitCode artifact = $artifactRelativePath journal_event_ids = @($journalEventIds) projection_updates = [ordered]@{ phase = $phase phase_status = $projectedPhaseStatus authoritative_sensor = $name rebuild_source = 'journal/events.jsonl' } } $resultEnvelope | ConvertTo-Json -Depth 12 if (-not $matched) { exit 1 } exit 0 |