Payload/scripts/kaden/utils/record-slice-approved.ps1

<#
.SYNOPSIS
    Record an operator slice approval on the harness journal and rebuild the gate projection.

.DESCRIPTION
    Appends a HumanGateDecided event with verdict "approved" and rebuilds
    .review-loop.json from the journal. The public harness-journal.ps1 instant
    action does not accept a payload, and the projection only copies verdict
    and notes from that payload.

    The feature folder and slice must already exist on the projection. This
    script refuses to invent a slice. Nothing is written unless -Apply is present.

.PARAMETER Feature
    Feature slug under docs/harness/features.

.PARAMETER Slice
    Existing slice id on that feature's .review-loop.json.

.PARAMETER Notes
    Operator notes stored on human_gate.notes. Default: "Operator accepted the slice."

.PARAMETER Summary
    Journal summary. Default: "Operator accepted <slice>".

.PARAMETER RepoRoot
    Repository root. Default: walk up from this script to scripts/kaden/harness-invoke.ps1.

.PARAMETER FeatureRoot
    Existing feature folder. Default: docs/harness/features/<Feature>.

.PARAMETER AllowArbitraryFeatureRoot
    Allow -FeatureRoot to point outside docs/harness/features. For a fixture, not a normal approval.

.PARAMETER Apply
    Append HumanGateDecided and rebuild the projection. Required for any write.

.PARAMETER Agent
    Journal actor agent. Default: atdd-developer.

.PARAMETER Tool
    Journal actor tool. Default: chat.

.PARAMETER ModelProvider
    Journal actor model provider. Default: cursor.

.PARAMETER ModelSlug
    Journal actor model slug. Default: unknown (HarnessJournal also reads HARNESS_MODEL_SLUG).

.PARAMETER ModelRole
    Journal actor model role. Default: primary.

.EXAMPLE
    pwsh -NoProfile -File .\scripts\kaden\utils\record-slice-approved.ps1 -Feature second-opinion-workflow -Slice advisory-assessment-routing -Apply

.EXAMPLE
    pwsh -NoProfile -File .\scripts\kaden\utils\record-slice-approved.ps1 -Feature second-opinion-workflow -Slice advisory-assessment-routing -Notes "Accepted after slice review." -ModelSlug grok-4.7-high-fast -Apply

.EXAMPLE
    Get-Help .\scripts\kaden\utils\record-slice-approved.ps1 -Parameter Apply
#>


#Requires -Version 5.1
[CmdletBinding(DefaultParameterSetName = 'Record')]
param(
    [Parameter(ParameterSetName = 'Record', Mandatory = $true)]
    [ValidateNotNullOrEmpty()]
    [ValidatePattern('^[A-Za-z0-9][A-Za-z0-9_-]*$')]
    [string]$Feature,

    [Parameter(ParameterSetName = 'Record', Mandatory = $true)]
    [ValidateNotNullOrEmpty()]
    [ValidatePattern('^[A-Za-z0-9][A-Za-z0-9_-]*$')]
    [string]$Slice,

    [Parameter(ParameterSetName = 'Record')]
    [string]$Notes = 'Operator accepted the slice.',

    [Parameter(ParameterSetName = 'Record')]
    [string]$Summary = '',

    [Parameter(ParameterSetName = 'Record')]
    [string]$RepoRoot = '',

    [Parameter(ParameterSetName = 'Record')]
    [string]$FeatureRoot = '',

    [Parameter(ParameterSetName = 'Record')]
    [switch]$AllowArbitraryFeatureRoot,

    [Parameter(ParameterSetName = 'Record')]
    [switch]$Apply,

    [Parameter(ParameterSetName = 'Record')]
    [string]$Agent = 'atdd-developer',

    [Parameter(ParameterSetName = 'Record')]
    [string]$Tool = 'chat',

    [Parameter(ParameterSetName = 'Record')]
    [ValidateSet('cursor', 'copilot', 'other')]
    [string]$ModelProvider = 'other',

    [Parameter(ParameterSetName = 'Record')]
    [string]$ModelSlug = 'unknown',

    [Parameter(ParameterSetName = 'Record')]
    [ValidateSet('primary', 'subagent')]
    [string]$ModelRole = 'primary',

    [Parameter(ParameterSetName = 'Help')]
    [Alias('h', 'help')]
    [switch]$ShowHelp
)

Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"

$cliKitManifest = Join-Path $PSScriptRoot "PsCliUiKit\PsCliUiKit.psd1"
if (-not (Test-Path -LiteralPath $cliKitManifest)) {
    throw "PsCliUiKit manifest not found: $cliKitManifest"
}
Import-Module -Name $cliKitManifest -Force

function Resolve-RecordSliceApprovedRepoRoot {
    param([string]$RequestedRoot)

    if (-not [string]::IsNullOrWhiteSpace($RequestedRoot)) {
        $resolved = (Resolve-Path -LiteralPath $RequestedRoot).Path
        $marker = Join-Path $resolved "scripts\kaden\harness-invoke.ps1"
        if (-not (Test-Path -LiteralPath $marker)) {
            throw "RepoRoot is not a Kaden checkout (missing scripts\kaden\harness-invoke.ps1): $resolved"
        }
        return $resolved
    }

    $current = $PSScriptRoot
    while ($true) {
        $marker = Join-Path $current "scripts\kaden\harness-invoke.ps1"
        if (Test-Path -LiteralPath $marker) {
            return $current
        }
        $parent = Split-Path $current -Parent
        if ([string]::IsNullOrWhiteSpace($parent) -or $parent -eq $current) {
            throw "Could not locate repository root (scripts\kaden\harness-invoke.ps1) from this script."
        }
        $current = $parent
    }
}

function Get-RecordSliceApprovedProjectionSlice {
    param(
        [Parameter(Mandatory = $true)]$Loop,
        [Parameter(Mandatory = $true)][string]$SliceId
    )

    foreach ($phase in @($Loop.phases)) {
        if ($null -eq $phase) { continue }
        $slices = $phase.PSObject.Properties["slices"]
        if ($null -eq $slices) { continue }
        foreach ($candidate in @($slices.Value)) {
            if ($null -eq $candidate) { continue }
            $idProp = $candidate.PSObject.Properties["id"]
            if ($null -ne $idProp -and [string]$idProp.Value -eq $SliceId) {
                return $candidate
            }
        }
    }
    return $null
}

try {
    if ($ShowHelp) {
        (Get-Help -Name $PSCommandPath -Full | Out-String -Width 100) | Out-Host
        return
    }

    Write-CliSection -Title "Recording slice approval" -Style info

    $root = Resolve-RecordSliceApprovedRepoRoot -RequestedRoot $RepoRoot
    Import-Module (Join-Path $root "scripts\kaden\harness\lib\HarnessContract.psm1") -Force
    Import-Module (Join-Path $root "scripts\kaden\harness\lib\HarnessJournal.psm1") -Force
    Import-Module (Join-Path $root "scripts\kaden\harness\lib\HarnessGateProjection.psm1") -Force

    $featureRootPath = Get-HarnessFeatureRoot -RepoRoot $root -Feature $Feature -FeatureRootOverride $FeatureRoot -AllowArbitraryFeatureRoot:$AllowArbitraryFeatureRoot
    $loopPath = Join-Path $featureRootPath ".review-loop.json"
    if (-not (Test-Path -LiteralPath $loopPath)) {
        throw "Projection not found: $loopPath"
    }

    $loop = Get-Content -LiteralPath $loopPath -Raw -Encoding utf8 | ConvertFrom-Json
    $projectionSlice = Get-RecordSliceApprovedProjectionSlice -Loop $loop -SliceId $Slice
    if ($null -eq $projectionSlice) {
        throw "Slice '$Slice' is not on the projection for feature '$Feature'. Refusing to invent it."
    }

    $gateProp = $projectionSlice.PSObject.Properties["human_gate"]
    if ($null -eq $gateProp -or $null -eq $gateProp.Value) {
        throw "Slice '$Slice' has no human_gate. The projection rebuild would drop this approval."
    }

    if ([string]::IsNullOrWhiteSpace($Summary)) {
        $Summary = "Operator accepted $Slice"
    }

    Write-CliKeyValue -Label "feature" -Value $Feature
    Write-CliKeyValue -Label "slice" -Value $Slice
    Write-CliKeyValue -Label "notes" -Value $Notes

    if (-not $Apply) {
        Write-CliStatus -State "Info" -Message "Nothing was written. Pass -Apply to record the approval."
        return
    }

    $actor = New-HarnessJournalActor -Agent $Agent -Tool $Tool -Provider $ModelProvider -Slug $ModelSlug -Role $ModelRole
    $activity = New-HarnessJournalActivity -Id (New-HarnessJournalActivityId) -Type "other" -Name "human-gate" -Role "instant"
    $payload = [pscustomobject]@{
        verdict = "approved"
        notes   = $Notes
    }

    $evt = Add-HarnessJournalEvent -FeatureRoot $featureRootPath -Feature $Feature -Slice $Slice -Kind "HumanGateDecided" -Activity $activity -Actor $actor -Outcome "PASS" -Summary $Summary -Payload $payload
    Update-HarnessGateProjectionFromJournal -FeatureRoot $featureRootPath -Feature $Feature -RepoRoot $root -Write | Out-Null
    Write-CliSection -Title "Slice approved" -Style success
    Write-CliKeyValue -Label "event" -Value "[info]$($evt.event_id)[-]"
} catch {
    Write-CliSection -Title "Slice approval failed" -Style "error"
    Write-CliStatus -State "Error" -Message $_.Exception.Message
    exit 2
} finally {
    Write-CliEnd
}