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
"@

    }
}

$resolvedModelSource = Resolve-HarnessPhaseSensorModelSource -SensorRunner `
    -ModelRole $ModelRole -ParentAgent $ParentAgent -Tool 'harness-script' -Agent 'harness-sensor.ps1'
$invocation = Format-HarnessModelInvocation -Agent 'harness-sensor.ps1' -Provider $ModelProvider -Slug $resolvedSlug `
    -Role $ModelRole -ParentAgent $(if ($ParentAgent) { $ParentAgent } else { $null }) `
    -Tool 'harness-script' -ModelSource $resolvedModelSource

$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).
"@

            }
            # DW-19-06: lock production edits while this sensor runs.
            Enter-HarnessSliceSensorLock -FeatureRoot $featureRootPath -Slice $Slice | Out-Null
            try {
                $result = Invoke-HarnessExternalCommand -RepoRoot $RepoRoot -Command $runner -Arguments $runArgs
            }
            finally {
                Exit-HarnessSliceSensorLock -FeatureRoot $featureRootPath -Slice $Slice
            }
            $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 `
    -ModelSource $resolvedModelSource

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')
            model_source      = $resolvedModelSource
        }) `
        -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
}