Payload/scripts/kaden/harness/harness-step.ps1
|
#Requires -Version 5.1 <# .SYNOPSIS Execute one gate-relevant harness step as a single transactional recording action. .DESCRIPTION Runs a named command, writes one evidence artifact, appends journal truth, and rebuilds the gate projection from that journal truth in the same invocation. Phase completion is only recorded when the observed exit code matches the expected exit code. That lets RED treat an expected failing test as a successful step without requiring an intermediate commit or a second recording command. .EXAMPLE .\scripts\kaden\harness-step.ps1 -Feature harness-event-journal -Slice tsr-journal-first-skeleton -Phase RED -Name red-test -Command powershell.exe -Arguments '-NoProfile','-Command','exit 1' -ExpectExit 1 .EXAMPLE .\scripts\kaden\harness-step.ps1 -Feature harness-event-journal -Slice tsr-journal-first-skeleton -Phase REFACTOR -Name verify-local -Command .\scripts\kaden\verify-local.ps1 -ExpectExit 0 #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Feature, [Parameter(Mandatory = $true)] [string]$Slice, [Parameter(Mandatory = $true)] [ValidateSet('RED', 'GREEN', 'MUTATE', 'REFACTOR', 'COMMIT')] [string]$Phase, [Parameter(Mandatory = $true)] [string]$Name, [Parameter(Mandatory = $true)] [string]$Command, [string[]]$Arguments = @(), [int]$ExpectExit = 0, [string]$TestSelector = '', [string[]]$TestFiles = @(), [string]$ObligationKey = '', [string]$AssertionText = '', [string]$EvidenceName = '', [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]$IncludeCommandTails, [switch]$AllowArbitraryFeatureRoot ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $modulePath = Join-Path $PSScriptRoot 'lib\HarnessContract.psm1' Import-Module $modulePath -Force Import-Module (Join-Path $PSScriptRoot 'lib\HarnessJournal.psm1') -Force Import-Module (Join-Path $PSScriptRoot 'lib\HarnessGateProjection.psm1') -Force $runtimeModule = Join-Path $PSScriptRoot 'lib\RepoRuntime.psm1' Import-Module $runtimeModule -Force Test-HarnessContractModuleReady if ([string]::IsNullOrWhiteSpace($RepoRoot)) { $RepoRoot = Get-HarnessRepoRoot -StartPath $PSScriptRoot } Initialize-RepoShellEnvironment -LogPrefix 'harness-step' | Out-Null $featureRootPath = Get-HarnessFeatureRoot -RepoRoot $RepoRoot -Feature $Feature -FeatureRootOverride $FeatureRoot -AllowArbitraryFeatureRoot:$AllowArbitraryFeatureRoot $resolvedSlug = Get-HarnessResolvedModelSlug -ModelSlug $ModelSlug # Reject synthetic RED theater (exit 1 with no test obligation) unless operator uses RED_RECOVERY skip on record-phase. if ($Phase -eq 'RED' -and $ExpectExit -ne 0) { $hasObligation = (@($TestFiles).Count -gt 0) ` -or (-not [string]::IsNullOrWhiteSpace($TestSelector)) ` -or (-not [string]::IsNullOrWhiteSpace($AssertionText)) ` -or (-not [string]::IsNullOrWhiteSpace($ObligationKey)) if (-not $hasObligation) { throw @" harness-step: RED with ExpectExit $ExpectExit requires -TestFiles, -TestSelector, -AssertionText, or -ObligationKey (observed failing tests). For interrupted recovery without a real failing run, record RED as skipped: harness-record-phase.ps1 -Phase RED -Status skipped -SkipReasonCode RED_RECOVERY -SkipRationale <why> Do not use a bare 'exit 1' wrapper as RED evidence. "@ } } $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 $snapshotTestFiles = @($TestFiles) $fileHashes = [ordered]@{} foreach ($testFile in $snapshotTestFiles) { if (-not (Test-Path -LiteralPath $testFile)) { throw "Transactional step test file not found: $testFile" } $fileHashes[$testFile] = (Get-FileHash -LiteralPath $testFile -Algorithm SHA256).Hash } $resolvedCommandPath = $null $commandHash = $null if (Test-Path -LiteralPath $Command) { $resolvedCommandPath = (Resolve-Path -LiteralPath $Command).Path $commandHash = (Get-FileHash -LiteralPath $resolvedCommandPath -Algorithm SHA256).Hash } $resolvedObligationKey = if ([string]::IsNullOrWhiteSpace($ObligationKey)) { if ([string]::IsNullOrWhiteSpace($TestSelector)) { $null } else { $TestSelector } } else { $ObligationKey } $supersedesEvent = Get-HarnessLatestJournalEventByName -FeatureRoot $featureRootPath -SliceId $Slice -Name $Name -Phase $Phase $startedAt = Get-HarnessIso8601UtcNow $result = Invoke-HarnessExternalCommand -RepoRoot $RepoRoot -Command $Command -Arguments $Arguments $completedAt = Get-HarnessIso8601UtcNow $matched = ($result.ExitCode -eq $ExpectExit) $stepVerdict = if ($matched) { 'PASS' } else { 'FAIL' } $summaryText = if ($matched) { "step $Name satisfied expected exit $ExpectExit" } else { "step $Name expected exit $ExpectExit but observed $($result.ExitCode)" } $stdoutTail = Format-HarnessSensorCommandTail -Lines $result.Stdout -MaxLines 40 -IncludeCommandTails:$IncludeCommandTails -ExitCode $result.ExitCode $stderrTail = Format-HarnessSensorCommandTail -Lines $result.Stderr -MaxLines 20 -IncludeCommandTails:$IncludeCommandTails -ExitCode $result.ExitCode $artifactBody = [ordered]@{ sensor_version = '1.0' name = $Name type = 'transactional' source = 'harness-step.ps1' phase = $Phase command = $Command arguments = @($Arguments) expected_exit_code = $ExpectExit exit_code = $result.ExitCode 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 = $ExpectExit exit_code = $result.ExitCode 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 = $Command 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\kaden\harness-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 = $ExpectExit actual_exit_code = $result.ExitCode 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 10 if (-not $matched) { exit 1 } exit 0 |