Payload/scripts/harness/harness-turn-start.ps1

#Requires -Version 5.1
<#
.SYNOPSIS
    Start a slice turn with journal + baseline verify-local evidence.

.DESCRIPTION
    This is the harness-first default entrypoint for implementation turns.
    It preflights verify-local (retry once) BEFORE ActivityStarted so a transient
    capture FAIL cannot poison turn-discipline baseline checks. On preflight
    success it records ActivityStarted, appends an authoritative verify-local
    sensor row, and can optionally mark one slice phase in_progress.

    For ATDD -Phase and -FeaturePhase turns, when a PASS verify-local sensor
    exists within -FreshVerifyMinutes and the production tree is clean, baseline
    verify is skipped and the fresh PASS is reused (same TTL + fingerprint).

.EXAMPLE
    .\scripts\harness-turn-start.ps1 -Feature saved-search-queries -Slice mo-save-list -Phase RED -Summary "start slice turn"
#>

[CmdletBinding()]
param(
    [Parameter(Mandatory = $true)]
    [string]$Feature,

    [Parameter(Mandatory = $true)]
    [string]$Slice,

    [ValidateSet('RED', 'GREEN', 'MUTATE', 'REFACTOR', 'COMMIT', '')]
    [string]$Phase = '',

    [ValidateSet('quality-gates', 'slice-review', '')]
    [string]$FeaturePhase = '',

    [string]$Summary = 'Turn start (baseline verify-local)',

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

    [string]$ModelSlug = 'unknown',

    [ValidateRange(1, 1440)]
    [int]$FreshVerifyMinutes = 20,

    [string]$FeatureRoot = '',

    [string]$RepoRoot = '',

    [switch]$AllowArbitraryFeatureRoot,

    # When set, skip `git status --short` and use -GitStatusShort (empty = clean). Pester CM-A seam.
    [switch]$OverrideGitStatus,

    [string]$GitStatusShort = ''
)

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

Import-Module (Join-Path $PSScriptRoot 'lib\RepoRuntime.psm1') -Force
Import-Module (Join-Path $PSScriptRoot 'lib\HarnessContract.psm1') -Force
Import-Module (Join-Path $PSScriptRoot 'lib\HarnessJournal.psm1') -Force

$recordPhase = Join-Path $PSScriptRoot 'harness-record-phase.ps1'
$sensorScript = Join-Path $PSScriptRoot 'harness-sensor.ps1'
$journalScript = Join-Path $PSScriptRoot 'harness-journal.ps1'
$verifyScript = Join-Path (Split-Path $PSScriptRoot -Parent) 'verify-local.ps1'

if ([string]::IsNullOrWhiteSpace($RepoRoot)) {
    $RepoRoot = Get-HarnessRepoRoot -StartPath $PSScriptRoot
}

Initialize-RepoShellEnvironment -LogPrefix 'harness-turn-start' | Out-Null

if (-not [string]::IsNullOrWhiteSpace($Phase) -and -not [string]::IsNullOrWhiteSpace($FeaturePhase)) {
    throw 'harness-turn-start: -Phase and -FeaturePhase are mutually exclusive.'
}

$featureRootPath = Get-HarnessFeatureRoot -RepoRoot $RepoRoot -Feature $Feature `
    -FeatureRootOverride $FeatureRoot -AllowArbitraryFeatureRoot:$AllowArbitraryFeatureRoot

$gitStatusShort = ''
if ($OverrideGitStatus) {
    $gitStatusShort = $GitStatusShort
}
else {
    Push-Location $RepoRoot
    try {
        $gitStatusShort = (& git status --short 2>$null | Out-String)
    }
    finally {
        Pop-Location
    }
}

$reuse = Resolve-HarnessTurnStartVerifyReuse `
    -FeaturePhase $FeaturePhase `
    -Phase $Phase `
    -FeatureRoot $featureRootPath `
    -FreshVerifyMinutes $FreshVerifyMinutes `
    -GitStatusShort $gitStatusShort `
    -RepoRoot $RepoRoot `
    -UtcNow ([datetime]::UtcNow)

$recordPhaseCommon = @{
    Feature        = $Feature
    Slice          = $Slice
    ModelProvider  = $ModelProvider
    ModelSlug      = $ModelSlug
    RepoRoot       = $RepoRoot
}
$childCommon = @{
    Feature        = $Feature
    Slice          = $Slice
    ModelProvider  = $ModelProvider
    ModelSlug      = $ModelSlug
    RepoRoot       = $RepoRoot
}
if (-not [string]::IsNullOrWhiteSpace($FeatureRoot)) {
    $recordPhaseCommon.FeatureRoot = $featureRootPath
    $recordPhaseCommon.AllowArbitraryFeatureRoot = $true
    $childCommon.FeatureRoot = $featureRootPath
    $childCommon.AllowArbitraryFeatureRoot = $true
}

$turnAlreadyOpen = Test-HarnessSliceTurnOpen -FeatureRoot $featureRootPath -Slice $Slice
if ($turnAlreadyOpen -and -not [string]::IsNullOrWhiteSpace($FeaturePhase)) {
    Write-Output '[harness-turn-start] turn already open; skipping ActivityStarted and baseline verify. Record FeaturePhase only.'
    & $recordPhase @recordPhaseCommon -FeaturePhase $FeaturePhase -FeaturePhaseStatus in_progress
    try {
        $ownReport = Get-HarnessOwnSliceReport -FeatureRoot $featureRootPath -Feature $Feature -Slice $Slice `
            -RepoRoot $RepoRoot
        $ownPath = Write-HarnessOwnSliceNextArtifact -FeatureRoot $featureRootPath -Report $ownReport
        Format-HarnessOwnSliceHumanReport -Report $ownReport -Header 'OWN after turn-start' | ForEach-Object { Write-Output $_ }
        Write-Output " Wrote: $ownPath"
        Write-Output ''
    }
    catch {
        Write-Warning "harness-turn-start: OWN context unavailable: $($_.Exception.Message)"
    }
    return
}

$readyReport = Assert-HarnessTurnStartSandboxReady -RepoRoot $RepoRoot
Write-Output ("[harness-turn-start] {0}" -f $readyReport.Summary)

$skippedBaseline = $false
if ($reuse.ShouldSkip) {
    $skippedBaseline = $true
    Write-Output "[harness-turn-start] $($reuse.Message)"
}
else {
    $preflight = Invoke-HarnessVerifyLocalBookendPreflight -RepoRoot $RepoRoot -VerifyScript $verifyScript
    if ($preflight.ExitCode -ne 0) {
        $stepLabel = if (-not [string]::IsNullOrWhiteSpace([string]$preflight.FailedStep)) {
            [string]$preflight.FailedStep
        }
        else {
            'verify-local'
        }
        throw "harness-turn-start: preflight verify-local failed at step '$stepLabel' after $($preflight.MaxAttempts) attempt(s). Fix the suite before starting a turn so baseline journal evidence stays PASS."
    }
}

& $journalScript @childCommon -Action start -Agent atdd-developer -Summary $Summary `
    -ActivityType turn -ActivityName start

if ($skippedBaseline) {
    Write-Output '[harness-turn-start] skipped baseline verify-local (fresh PASS reused); no new verify-local sensor written'
    $reusedSensor = $reuse.Sensor
    $reusedArtifact = [string](Get-HarnessProperty -Object $reusedSensor -Name 'RelativeName')
    $reusedActivity = New-HarnessJournalActivity -Id (New-HarnessJournalActivityId) -Type 'sensor' -Name 'verify-local' -Role 'instant'
    $reusedActor = New-HarnessJournalActor -Agent 'harness-turn-start' -ParentAgent $null -Tool 'harness-script' `
        -Provider $ModelProvider -Slug $ModelSlug -Role 'primary'
    Add-HarnessJournalEvent -FeatureRoot $featureRootPath -Feature $Feature -Slice $Slice `
        -Kind 'SensorRunRecorded' -Activity $reusedActivity -Actor $reusedActor -Outcome 'PASS' `
        -Summary ("reused fresh verify-local sensor {0}" -f $reusedArtifact) `
        -Payload ([pscustomobject]@{
            name              = 'verify-local'
            type              = 'computational'
            verdict           = 'PASS'
            evidence_artifact = ('sensors/' + $reusedArtifact)
            skip_reason_code  = 'REUSED_FRESH_SENSOR'
            skip_rationale    = 'fresh-pass-reuse'
        }) `
        -Refs ([pscustomobject]@{
            amends_event_id = $null
            pair_event_id   = $null
            artifact        = ('sensors/' + $reusedArtifact)
            command         = 'harness-turn-start.ps1'
            inner_command   = 'reused fresh verify-local sensor'
        }) | Out-Null
}
else {
    & $sensorScript @childCommon -Run verify-local -Append
    $sensorExit = $LASTEXITCODE

    # Immediate baseline discipline check -- fail fast instead of 60 minutes later.
    # harness-sensor.ps1 exits 1 when verify-local returned FAIL; we surface that now
    # rather than letting a poisoned baseline propagate silently through RED -> REFACTOR.
    if ($sensorExit -ne 0) {
        Write-Warning 'harness-turn-start: verify-local sensor recorded FAIL. The turn ActivityStarted is in the journal but the baseline is poisoned.'
        Write-Warning 'Fix: resolve the suite failure (npm test / npm run lint), then run harness-turn-stop.ps1 to close this turn, and start a fresh turn with harness-turn-start.ps1.'
        throw 'harness-turn-start: baseline verify-local FAIL -- turn is open but poisoned. See warnings above.'
    }
}

if (-not [string]::IsNullOrWhiteSpace($Phase)) {
    & $recordPhase @recordPhaseCommon -Phase $Phase -Status in_progress
}
elseif (-not [string]::IsNullOrWhiteSpace($FeaturePhase)) {
    & $recordPhase @recordPhaseCommon -FeaturePhase $FeaturePhase -FeaturePhaseStatus in_progress
}

# OWN context -- same terminal blob as ActivityStarted so agents see NEXT without a second memory hop.
try {
    $ownReport = Get-HarnessOwnSliceReport -FeatureRoot $featureRootPath -Feature $Feature -Slice $Slice `
        -RepoRoot $RepoRoot
    $ownPath = Write-HarnessOwnSliceNextArtifact -FeatureRoot $featureRootPath -Report $ownReport
    Format-HarnessOwnSliceHumanReport -Report $ownReport -Header 'OWN after turn-start' | ForEach-Object { Write-Output $_ }
    Write-Output " Wrote: $ownPath"
    Write-Output ''
}
catch {
    Write-Warning "harness-turn-start: OWN context unavailable: $($_.Exception.Message)"
}