Payload/scripts/kaden/harness/harness-sensor.ps1
|
#Requires -Version 5.1 <# .SYNOPSIS Run computational sensors; write evidence under sensors/; append journal rows when -Append. .DESCRIPTION Executes verify-local, harness-run-slice-tests, lint:tests, mutation:diff, or validate-review-loop. Each run writes JSON under docs/harness/features/<slug>/sensors/ and appends SensorRunRecorded / GateCheckRun to the event journal so the ledger records FAIL as well as PASS. With -Append, the script also rebuilds the gate projection from that journal truth. See docs/kaden/harness-journal.md. Command stdout/stderr tails are empty by default; use -IncludeCommandTails still forces redacted tails on PASS. FAIL sensors include redacted tails by default when output exists. Model slug: -ModelSlug or env HARNESS_MODEL_SLUG. .PARAMETER RunBundle Standard pre-review_ready bundle: verify-local (includes lint:tests), then mutation:diff when verify PASS and src/ changed. Verify FAIL omits mutation. No src/ change records a structured mutation skip without Stryker. .EXAMPLE .\scripts\kaden\harness-sensor.ps1 -Feature saved-search-queries -Slice mo-save-list -Run verify-local -Append .EXAMPLE .\scripts\kaden\harness-sensor.ps1 -Feature saved-search-queries -Slice mo-save-list -RunBundle -Append #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Feature, [Parameter(Mandatory = $true)] [string]$Slice, [string[]]$Run = @(), [switch]$RunBundle, [switch]$Append, [string]$FeatureRoot = '', [string]$RepoRoot = '', [ValidateSet('cursor', 'copilot', 'other')] [string]$ModelProvider = 'cursor', [string]$ModelSlug = 'unknown', [ValidateSet('primary', 'subagent')] [string]$ModelRole = 'primary', [string]$ParentAgent = '', [switch]$IncludeCommandTails, [switch]$AllowArbitraryFeatureRoot, # Product slices: pass Vitest paths when -Run harness-run-slice-tests. # harness-event-journal omits -TestFiles (runs tests/harness/run-harness-tests.ps1). [string[]]$TestFiles = @() ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' if ($RunBundle) { $Run = @('verify-local', 'mutation:diff') } if ($Run.Count -eq 0) { throw 'Specify -Run <sensors> or -RunBundle.' } $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-sensor' | ForEach-Object { Write-Output $_ } $resolvedSlug = Get-HarnessResolvedModelSlug -ModelSlug $ModelSlug $featureRootPath = Get-HarnessFeatureRoot -RepoRoot $RepoRoot -Feature $Feature -FeatureRootOverride $FeatureRoot -AllowArbitraryFeatureRoot:$AllowArbitraryFeatureRoot $TestFiles = @(Expand-HarnessSensorTestFiles -TestFiles $TestFiles) $needsAtddGate = $RunBundle -or (@($Run) -contains 'mutation:diff') if ($needsAtddGate) { $loopDoc = Get-ReviewLoopDocument -FeatureRoot $featureRootPath if (-not (Test-HarnessSliceAtddPhasesCompleteForMutation -LoopData $loopDoc.Data -SliceId $Slice)) { throw @" harness-sensor: mutation:diff / -RunBundle blocked until RED and GREEN are complete (or skipped) with evidence for slice '$Slice'. Run: pwsh -File scripts/kaden/harness-invoke.ps1 harness-own-slice-status.ps1 -Feature $Feature -Slice $Slice "@ } } $invocation = Format-HarnessModelInvocation -Agent 'harness-sensor.ps1' -Provider $ModelProvider -Slug $resolvedSlug -Role $ModelRole -ParentAgent $(if ($ParentAgent) { $ParentAgent } else { $null }) $createdSensors = @() $bundleVerifyVerdict = $null $gitStatusShort = '' if ($RunBundle) { Push-Location $RepoRoot try { $gitStatusShort = (& git status --short 2>$null | Out-String) } finally { Pop-Location } } foreach ($sensorName in $Run) { $tsFile = Get-HarnessTimestampForFilename if ($RunBundle -and $sensorName -eq 'mutation:diff') { $mutPlan = Get-HarnessRunBundleMutationPlan -VerifyVerdict $bundleVerifyVerdict -RepoRoot $RepoRoot if ($mutPlan.Action -eq 'omit') { continue } if ($mutPlan.Action -eq 'skip') { $rel = Get-HarnessSensorArtifactRelativePath -Name 'mutation-diff' -TimestampForFile $tsFile $pair = Get-HarnessSensorInvocationPair -RunName 'mutation:diff' $skipCode = [string]$mutPlan.SkipReasonCode $skipRat = [string]$mutPlan.SkipRationale $innerSkip = 'RunBundle skipped npm run mutation:diff (NO_PRODUCTION_CHANGES)' $body = [ordered]@{ sensor_version = '1.0' name = 'mutation:diff' type = 'computational' source = 'harness-sensor.ps1' command = $pair.Command inner_command = $innerSkip exit_code = 0 verdict = 'SKIPPED' started_at = (Get-HarnessIso8601UtcNow) completed_at = (Get-HarnessIso8601UtcNow) summary = "mutation:diff SKIPPED ($skipCode)" skip_reason_code = $skipCode skip_rationale = $skipRat stdout_tail = '' stderr_tail = '' invocation = $invocation } Export-HarnessSensorArtifactFile -FeatureRoot $featureRootPath -Payload @{ relative_path = $rel; body = $body } | Out-Null $createdSensors += Format-HarnessSensorLoopRecord -Invocation $invocation -Name 'mutation:diff' -Type 'computational' -Verdict 'SKIPPED' ` -EvidenceArtifact $rel -EvidenceSummary $body.summary -ExitCode 0 -Command $pair.Command -InnerCommand $innerSkip ` -SkipReasonCode $skipCode -SkipRationale $skipRat continue } } # RunBundle reuse: fresh PASS/SKIPPED sensors on a clean tree are recorded, not re-invoked. if ($RunBundle -and $sensorName -in @('verify-local', 'mutation:diff')) { $accept = if ($sensorName -eq 'verify-local') { @('PASS') } else { @('PASS', 'SKIPPED') } $decision = Resolve-HarnessSensorReuse ` -FeatureRoot $featureRootPath ` -SensorName $sensorName ` -AcceptVerdicts $accept ` -FreshMinutes 20 ` -GitStatusShort $gitStatusShort ` -RepoRoot $RepoRoot ` -UtcNow ([datetime]::UtcNow) ` -RequireJournalBacking if ($decision.ShouldReuse) { Write-Output "[harness-sensor] $($decision.Message)" $reusedRel = "sensors/$($decision.Sensor.RelativeName)" $rel = Get-HarnessSensorArtifactRelativePath -Name $sensorName -TimestampForFile $tsFile $pair = Get-HarnessSensorInvocationPair -RunName $sensorName $cmdLabel = $pair.Command $exitCode = if ($decision.Sensor.Verdict -eq 'PASS') { 0 } else { 0 } $body = [ordered]@{ sensor_version = '1.0' name = $sensorName type = 'computational' source = 'harness-sensor.ps1' command = $cmdLabel inner_command = $pair.InnerCommand exit_code = $exitCode verdict = $decision.Sensor.Verdict started_at = (Get-HarnessIso8601UtcNow) completed_at = (Get-HarnessIso8601UtcNow) summary = $decision.Message skip_reason_code = 'REUSED_FRESH_SENSOR' skip_rationale = "Reused fresh sensor artifact $reusedRel within TTL on a clean tree" reused_artifact = $reusedRel stdout_tail = '' stderr_tail = '' invocation = $invocation } Export-HarnessSensorArtifactFile -FeatureRoot $featureRootPath -Payload @{ relative_path = $rel; body = $body } | Out-Null $createdSensors += Format-HarnessSensorLoopRecord -Invocation $invocation -Name $sensorName -Type 'computational' ` -Verdict $decision.Sensor.Verdict -EvidenceArtifact $reusedRel -EvidenceSummary $decision.Message ` -ExitCode $exitCode -Command $cmdLabel -InnerCommand $pair.InnerCommand -SkipReasonCode 'REUSED_FRESH_SENSOR' ` -SkipRationale "Reused fresh sensor artifact $reusedRel within TTL on a clean tree" if ($sensorName -eq 'verify-local') { $bundleVerifyVerdict = [string]$decision.Sensor.Verdict } continue } } switch ($sensorName) { 'verify-local' { $cmd = Join-Path $RepoRoot 'scripts\kaden\verify-local.ps1' $startedAt = Get-HarnessIso8601UtcNow $result = Invoke-HarnessExternalCommand -RepoRoot $RepoRoot -Command $cmd -Arguments @() # Capture-failure detection: when the process exits non-zero AND produces # near-empty output (both stdout + stderr < 200 bytes), the process likely # failed to initialize (e.g. fnm PATH not inherited by grandchild). Retry once # after a brief pause before writing any journal evidence. if ($result.ExitCode -ne 0) { $outLen = if ($result.Stdout) { $result.Stdout.Length } else { 0 } $errLen = if ($result.Stderr) { $result.Stderr.Length } else { 0 } if (($outLen + $errLen) -lt 200) { Write-Warning "[harness-sensor] verify-local exited $($result.ExitCode) with minimal output -- likely capture-failure. Retrying once in 3 seconds..." Start-Sleep -Seconds 3 $result = Invoke-HarnessExternalCommand -RepoRoot $RepoRoot -Command $cmd -Arguments @() } } $verdict = if ($result.ExitCode -eq 0) { 'PASS' } else { 'FAIL' } $rel = Get-HarnessSensorArtifactRelativePath -Name 'verify-local' -TimestampForFile $tsFile $fields = Get-HarnessVerifyLocalSensorFieldSet -ExitCode $result.ExitCode -StdoutLines @($result.Stdout) -StderrLines @($result.Stderr) -IncludeCommandTails:$IncludeCommandTails -MaxScopedLines 60 $summaryText = [string]$fields.Summary $stdoutTail = [string]$fields.StdoutTail $stderrTail = [string]$fields.StderrTail $prodFp = Get-HarnessProductionTreeFingerprint -RepoRoot $RepoRoot $prodDirty = Test-HarnessGitProductionTreeDirty -RepoRoot $RepoRoot -GitStatusShort $gitStatusShort $pair = Get-HarnessSensorInvocationPair -RunName 'verify-local' $body = [ordered]@{ sensor_version = '1.0' name = 'verify-local' type = 'computational' source = 'harness-sensor.ps1' command = $pair.Command inner_command = $pair.InnerCommand exit_code = $result.ExitCode verdict = $verdict started_at = $startedAt completed_at = (Get-HarnessIso8601UtcNow) summary = $summaryText failed_step = $fields.FailedStep steps_ran = @($fields.StepsRan) stdout_tail = $stdoutTail stderr_tail = $stderrTail production_tree_fingerprint = $prodFp production_tree_dirty = $prodDirty invocation = $invocation } Export-HarnessSensorArtifactFile -FeatureRoot $featureRootPath -Payload @{ relative_path = $rel; body = $body } | Out-Null $createdSensors += Format-HarnessSensorLoopRecord -Invocation $invocation -Name 'verify-local' -Type 'computational' -Verdict $verdict ` -EvidenceArtifact $rel -EvidenceSummary $body.summary -ExitCode $result.ExitCode -Command $pair.Command -InnerCommand $pair.InnerCommand $bundleVerifyVerdict = $verdict } 'lint:tests' { $startedAt = Get-HarnessIso8601UtcNow $result = Invoke-HarnessExternalCommand -RepoRoot $RepoRoot -Command 'npm' -Arguments @('run', 'lint:tests') $verdict = if ($result.ExitCode -eq 0) { 'PASS' } else { 'FAIL' } $rel = Get-HarnessSensorArtifactRelativePath -Name 'lint-tests' -TimestampForFile $tsFile $summaryText = if ($verdict -eq 'PASS') { 'lint:tests passed' } else { 'lint:tests failed' } $stdoutTail = Format-HarnessSensorCommandTail -Lines $result.Stdout -MaxLines 40 -IncludeCommandTails:$IncludeCommandTails -ExitCode $result.ExitCode $pair = Get-HarnessSensorInvocationPair -RunName 'lint:tests' $body = [ordered]@{ sensor_version = '1.0' name = 'lint:tests' type = 'computational' source = 'harness-sensor.ps1' command = $pair.Command inner_command = $pair.InnerCommand exit_code = $result.ExitCode verdict = $verdict started_at = $startedAt completed_at = (Get-HarnessIso8601UtcNow) summary = $summaryText stdout_tail = $stdoutTail invocation = $invocation } Export-HarnessSensorArtifactFile -FeatureRoot $featureRootPath -Payload @{ relative_path = $rel; body = $body } | Out-Null $createdSensors += Format-HarnessSensorLoopRecord -Invocation $invocation -Name 'lint:tests' -Type 'computational' -Verdict $verdict ` -EvidenceArtifact $rel -EvidenceSummary $body.summary -ExitCode $result.ExitCode -Command $pair.Command -InnerCommand $pair.InnerCommand } 'mutation:diff' { $startedAt = Get-HarnessIso8601UtcNow $result = Invoke-HarnessExternalCommand -RepoRoot $RepoRoot -Command 'npm' -Arguments @('run', 'mutation:diff') $resolved = Resolve-HarnessMutationDiffSensorResult -ExitCode $result.ExitCode -StdoutLines @($result.Stdout) -RepoRoot $RepoRoot $verdict = $resolved.Verdict $skipCode = [string]$resolved.SkipReasonCode $skipRat = [string]$resolved.SkipRationale $rel = Get-HarnessSensorArtifactRelativePath -Name 'mutation-diff' -TimestampForFile $tsFile $stdoutTail = Format-HarnessSensorCommandTail -Lines $result.Stdout -MaxLines 40 -IncludeCommandTails:$IncludeCommandTails -ExitCode $result.ExitCode $summaryText = if ($verdict -eq 'SKIPPED') { "mutation:diff SKIPPED ($skipCode)" } else { "mutation:diff exit $($result.ExitCode)" } $pair = Get-HarnessSensorInvocationPair -RunName 'mutation:diff' $body = [ordered]@{ sensor_version = '1.0' name = 'mutation:diff' type = 'computational' source = 'harness-sensor.ps1' command = $pair.Command inner_command = $pair.InnerCommand exit_code = $result.ExitCode verdict = $verdict started_at = $startedAt completed_at = (Get-HarnessIso8601UtcNow) summary = $summaryText stdout_tail = $stdoutTail invocation = $invocation } if ($verdict -eq 'SKIPPED') { $body['skip_reason_code'] = $skipCode $body['skip_rationale'] = $skipRat } Export-HarnessSensorArtifactFile -FeatureRoot $featureRootPath -Payload @{ relative_path = $rel; body = $body } | Out-Null $createdSensors += Format-HarnessSensorLoopRecord -Invocation $invocation -Name 'mutation:diff' -Type 'computational' -Verdict $verdict ` -EvidenceArtifact $rel -EvidenceSummary $body.summary -ExitCode $result.ExitCode -Command $pair.Command -InnerCommand $pair.InnerCommand ` -SkipReasonCode $skipCode -SkipRationale $skipRat } 'harness-run-slice-tests' { $startedAt = Get-HarnessIso8601UtcNow $isHarnessFeature = ($Feature -like 'harness-*') $runner = $null $runArgs = @() $commandLabel = '' if (@($TestFiles).Count -gt 0) { $runner = Join-Path $RepoRoot 'scripts\kaden\harness\harness-run-slice-tests.ps1' $csv = Join-HarnessSensorTestFilesArg -TestFiles @($TestFiles) $runArgs = @('-TestFiles', $csv) $commandLabel = '.\scripts\kaden\harness\harness-run-slice-tests.ps1' } elseif ($isHarnessFeature) { $runner = Join-Path $RepoRoot 'tests\harness\run-harness-tests.ps1' $commandLabel = '.\tests\harness\run-harness-tests.ps1' } else { throw @" harness-sensor: -Run harness-run-slice-tests requires -TestFiles for product slices (Feature '$Feature'). Example: pwsh -File scripts/kaden/harness-invoke.ps1 harness-sensor.ps1 -Feature <slug> -Slice <id> -Run harness-run-slice-tests -TestFiles tests/product-lines/ck/foo.test.js -Append -ModelProvider <p> -ModelSlug <s> For harness-* slices, omit -TestFiles (Pester suite runs automatically). "@ } $result = Invoke-HarnessExternalCommand -RepoRoot $RepoRoot -Command $runner -Arguments $runArgs $verdict = if ($result.ExitCode -eq 0) { 'PASS' } else { 'FAIL' } $rel = Get-HarnessSensorArtifactRelativePath -Name 'harness-run-slice-tests' -TimestampForFile $tsFile $summaryText = Get-HarnessSliceTestSensorSummary -ExitCode $result.ExitCode -StdoutLines @($result.Stdout) -StderrLines @($result.Stderr) $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 $pair = Get-HarnessSensorInvocationPair -RunName 'harness-run-slice-tests' -InnerCommand $commandLabel $body = [ordered]@{ sensor_version = '1.0' name = 'harness-run-slice-tests' type = 'computational' source = 'harness-sensor.ps1' command = $pair.Command inner_command = $pair.InnerCommand exit_code = $result.ExitCode verdict = $verdict started_at = $startedAt completed_at = (Get-HarnessIso8601UtcNow) summary = $summaryText stdout_tail = $stdoutTail stderr_tail = $stderrTail test_files = @($TestFiles) invocation = $invocation } Export-HarnessSensorArtifactFile -FeatureRoot $featureRootPath -Payload @{ relative_path = $rel; body = $body } | Out-Null $createdSensors += Format-HarnessSensorLoopRecord -Invocation $invocation -Name 'harness-run-slice-tests' -Type 'computational' -Verdict $verdict ` -EvidenceArtifact $rel -EvidenceSummary $body.summary -ExitCode $result.ExitCode -Command $pair.Command -InnerCommand $pair.InnerCommand } 'validate-review-loop' { $startedAt = Get-HarnessIso8601UtcNow $validation = Invoke-HarnessReviewLoopValidation -RepoRoot $RepoRoot -FeatureRoot $featureRootPath -SliceId $Slice -RequireReviewReady $verdict = $validation.verdict $rel = Get-HarnessSensorArtifactRelativePath -Name 'validate-review-loop' -TimestampForFile $tsFile $exitCode = if ($verdict -eq 'PASS') { 0 } else { 1 } $pair = Get-HarnessSensorInvocationPair -RunName 'validate-review-loop' $body = [ordered]@{ sensor_version = '1.0' name = 'validate-review-loop' type = 'computational' source = 'harness-sensor.ps1' command = $pair.Command inner_command = $pair.InnerCommand exit_code = $exitCode verdict = $verdict started_at = $startedAt completed_at = (Get-HarnessIso8601UtcNow) summary = "validation $verdict ($($validation.findings.Count) findings)" findings = $validation.findings invocation = $invocation } Export-HarnessSensorArtifactFile -FeatureRoot $featureRootPath -Payload @{ relative_path = $rel; body = $body } | Out-Null $sensorRecord = Format-HarnessSensorLoopRecord -Invocation $invocation -Name 'validate-review-loop' -Type 'computational' -Verdict $verdict ` -EvidenceArtifact $rel -EvidenceSummary $body.summary -ExitCode $exitCode -Command $pair.Command -InnerCommand $pair.InnerCommand $sensorRecord | Add-Member -NotePropertyName 'gate_validation_pass' -NotePropertyValue ($verdict -eq 'PASS') -Force $createdSensors += $sensorRecord } default { throw "Unknown sensor: $sensorName" } } } $journalParent = if ($ParentAgent) { $ParentAgent } else { $null } $journalActor = New-HarnessJournalActor -Agent 'harness-sensor.ps1' -ParentAgent $journalParent ` -Tool 'harness-script' -Provider $ModelProvider -Slug $resolvedSlug -Role $ModelRole foreach ($sensor in $createdSensors) { $actId = New-HarnessJournalActivityId $activity = New-HarnessJournalActivity -Id $actId -Type 'sensor' -Name $sensor.name -Role 'instant' $kind = if ($sensor.name -eq 'validate-review-loop') { 'GateCheckRun' } else { 'SensorRunRecorded' } Add-HarnessJournalEvent -FeatureRoot $featureRootPath -Feature $Feature -Slice $Slice ` -Kind $kind -Activity $activity -Actor $journalActor -Outcome $sensor.verdict ` -Summary $sensor.evidence_summary ` -Payload ([pscustomobject]@{ name = $sensor.name type = $sensor.type verdict = $sensor.verdict evidence_artifact = $sensor.evidence_artifact exit_code = $sensor.exit_code command = $sensor.command inner_command = (Get-HarnessProperty -Object $sensor -Name 'inner_command') skip_reason_code = (Get-HarnessProperty -Object $sensor -Name 'skip_reason_code') skip_rationale = (Get-HarnessProperty -Object $sensor -Name 'skip_rationale') }) ` -Refs ([pscustomobject]@{ amends_event_id = $null pair_event_id = $null artifact = $sensor.evidence_artifact command = $sensor.command inner_command = (Get-HarnessProperty -Object $sensor -Name 'inner_command') }) | Out-Null # When validate-review-loop passes RequireReviewReady, append a GateValidationPassed event. # The projection builder promotes ownership_status to review_ready from this event, making # it durable across future projection rebuilds (OwnershipChanged cannot elevate to review_ready). if ($sensor.name -eq 'validate-review-loop' -and $sensor.verdict -eq 'PASS') { $gvpActivity = New-HarnessJournalActivity -Id (New-HarnessJournalActivityId) -Type 'gate' -Name 'validate-review-loop' -Role 'instant' Add-HarnessJournalEvent -FeatureRoot $featureRootPath -Feature $Feature -Slice $Slice ` -Kind 'GateValidationPassed' -Activity $gvpActivity -Actor $journalActor -Outcome 'PASS' ` -Summary 'validate-review-loop -RequireReviewReady PASS -- slice is review_ready' ` -Payload ([pscustomobject]@{ gate = 'validate-review-loop' flags = @('RequireReviewReady') evidence_artifact = $sensor.evidence_artifact }) ` -Refs ([pscustomobject]@{ amends_event_id = $null pair_event_id = $null artifact = $sensor.evidence_artifact command = $sensor.command inner_command = (Get-HarnessProperty -Object $sensor -Name 'inner_command') }) | Out-Null } } if ($Append) { Update-HarnessGateProjectionFromJournal -FeatureRoot $featureRootPath -Feature $Feature -RepoRoot $RepoRoot -Write | Out-Null # After GateValidationPassed (or any Append), refresh NEXT so review_ready is visible without a manual own-slice. Sync-HarnessOwnSliceNextArtifact -FeatureRoot $featureRootPath -Feature $Feature -Slice $Slice -RepoRoot $RepoRoot | Out-Null } $createdSensors | ConvertTo-Json -Depth 10 # Exit non-zero when any verify-local sensor failed so callers ($LASTEXITCODE) can gate on it. # Other sensors (lint:tests, mutation:diff) are informational; only verify-local blocks. $verifyFailed = @($createdSensors | Where-Object { $_.name -eq 'verify-local' -and $_.verdict -ne 'PASS' }) if ($verifyFailed.Count -gt 0) { exit 1 } |