scripts/internal/continuous-co-review/review-harness-contract.ps1

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

if (-not (Get-Command -Name 'Get-ContinuousCoReviewProductionHarnessDefinition' -ErrorAction SilentlyContinue)) {
    . (Join-Path $PSScriptRoot 'reviewer-host-catalog.ps1')
}
if (-not (Get-Command -Name 'Get-ReviewAuthorityCandidateLimits' -ErrorAction SilentlyContinue)) {
    . (Join-Path $PSScriptRoot 'review-authority-core.ps1')
}

# Shared synchronous process/file contract for production review harnesses. Reviewers write one bounded
# candidate JSON object to the controller-owned path. Stdout may be observed as transient activity, but it
# is never parsed, extracted, or copied into candidate authority.

function Get-ReviewHarnessContractLimits {
    $authority = Get-ReviewAuthorityCandidateLimits
    return [pscustomobject][ordered]@{
        schema_version = '1.0'
        max_prompt_template_bytes = 32768
        max_rendered_prompt_bytes = 65536
        max_candidate_bytes = $authority.max_candidate_bytes
        advertised_summary_characters = [int][Math]::Floor($authority.max_summary_characters / 2)
        advertised_findings = [int][Math]::Floor($authority.max_findings / 2)
        advertised_local_id_characters = [int][Math]::Floor(($authority.max_local_id_characters * 3) / 4)
        advertised_title_characters = [int][Math]::Floor(($authority.max_title_characters * 4) / 5)
        advertised_description_characters = [int][Math]::Floor(($authority.max_description_characters * 3) / 4)
        advertised_location_characters = [int][Math]::Floor(($authority.max_location_characters * 4) / 5)
    }
}

function Get-ReviewFilePrimaryPromptPlaceholders {
    return @(
        '__RUN_ID__', '__TARGET_DIGEST__', '__CANDIDATE_RESULT_PATH__', '__REVIEW_SCOPE__', '__DEADLINE__',
        '__MAX_SUMMARY_CHARACTERS__', '__MAX_FINDINGS__', '__MAX_LOCAL_ID_CHARACTERS__',
        '__MAX_TITLE_CHARACTERS__', '__MAX_DESCRIPTION_CHARACTERS__', '__MAX_LOCATION_CHARACTERS__'
    )
}

function Test-ReviewFilePrimaryPromptTemplate {
    [CmdletBinding()]
    param([Parameter(Mandatory)][AllowEmptyString()][string]$Template)

    $limits = Get-ReviewHarnessContractLimits
    $errors = [Collections.Generic.List[string]]::new()
    $bytes = [Text.Encoding]::UTF8.GetByteCount($Template)
    if ($bytes -gt $limits.max_prompt_template_bytes) { $errors.Add("prompt-template-too-large:$($limits.max_prompt_template_bytes)") | Out-Null }

    $required = Get-ReviewFilePrimaryPromptPlaceholders
    foreach ($placeholder in $required) {
        $count = ([regex]::Matches($Template, [regex]::Escape($placeholder))).Count
        if ($count -ne 1) { $errors.Add(('prompt-placeholder-count:{0}:{1}' -f $placeholder, $count)) | Out-Null }
    }
    foreach ($match in [regex]::Matches($Template, '__[A-Z0-9_]+__')) {
        if ([string]$match.Value -cnotin $required) { $errors.Add(('prompt-placeholder-unknown:' + [string]$match.Value)) | Out-Null }
    }
    foreach ($rule in @(
        @{ name = 'raw-json-only'; pattern = '(?is)ONLY\s+the\s+raw\s+JSON\s+object' },
        @{ name = 'no-prose'; pattern = '(?is)no\s+prose' },
        @{ name = 'no-fences'; pattern = '(?is)no\s+Markdown\s+fences' },
        @{ name = 'stdout-non-authority'; pattern = '(?is)stdout.+never\s+parsed\s+for\s+authority' },
        @{ name = 'source-read-only'; pattern = '(?is)do\s+not\s+modify\s+the\s+source' },
        @{ name = 'host-tool-posture'; pattern = '(?is)inspection\s+tools\s+that\s+this\s+host\s+actually\s+exposes.+some\s+hosts\s+mechanically\s+expose\s+only\s+Read.+Glob.+Grep.+Write.+other\s+hosts\s+expose\s+general\s+tools.+shell\s+tool.+read-only\s+inspection.+sole\s+permitted\s+mutation.+exact\s+candidate\s+result\s+path' },
        @{ name = 'single-reviewer-session'; pattern = '(?is)do\s+not\s+delegate\s+to\s+subagents\s+or\s+start\s+other\s+model-backed\s+reviewers' },
        @{ name = 'risk-based-completion'; pattern = '(?is)risk-based\s+inspection.+not\s+required\s+to\s+open\s+every\s+file.+complete\s+candidate.+requested\s+review\s+scope.+high-risk\s+check' },
        @{ name = 'location-string-type'; pattern = '(?is)`?location`?.+when\s+present.+plain\s+JSON\s+string.+never\s+an\s+object.+array.+number.+boolean' },
        @{ name = 'summary-budget'; pattern = '(?is)summary\s+at\s+most\s+__MAX_SUMMARY_CHARACTERS__\s+characters' },
        @{ name = 'finding-budgets'; pattern = '(?is)no\s+more\s+than\s+__MAX_FINDINGS__\s+findings.+local_id\s+at\s+most\s+__MAX_LOCAL_ID_CHARACTERS__\s+characters.+title\s+at\s+most\s+__MAX_TITLE_CHARACTERS__.+description\s+at\s+most\s+__MAX_DESCRIPTION_CHARACTERS__.+location\s+at\s+most\s+__MAX_LOCATION_CHARACTERS__.+shorten\s+prose.+never\s+truncate\s+the\s+JSON\s+object' }
    )) {
        if ($Template -notmatch $rule.pattern) { $errors.Add(('prompt-contract-missing:' + $rule.name)) | Out-Null }
    }
    return [pscustomobject]@{ valid = ($errors.Count -eq 0); errors = @($errors); byte_count = $bytes }
}

function Render-ReviewFilePrimaryPrompt {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$Template,
        [Parameter(Mandatory)]$Invocation
    )
    $validation = Test-ReviewFilePrimaryPromptTemplate -Template $Template
    if (-not $validation.valid) { throw ('review-file-primary-prompt-invalid:' + ($validation.errors -join ',')) }
    $limits = Get-ReviewHarnessContractLimits
    $values = @{
        '__RUN_ID__' = [string]$Invocation.run_id
        '__TARGET_DIGEST__' = [string]$Invocation.target_digest
        '__CANDIDATE_RESULT_PATH__' = [IO.Path]::GetFullPath([string]$Invocation.candidate_result_path)
        '__REVIEW_SCOPE__' = [string]$Invocation.review_scope
        '__DEADLINE__' = [string]$Invocation.deadline
        '__MAX_SUMMARY_CHARACTERS__' = [string]$limits.advertised_summary_characters
        '__MAX_FINDINGS__' = [string]$limits.advertised_findings
        '__MAX_LOCAL_ID_CHARACTERS__' = [string]$limits.advertised_local_id_characters
        '__MAX_TITLE_CHARACTERS__' = [string]$limits.advertised_title_characters
        '__MAX_DESCRIPTION_CHARACTERS__' = [string]$limits.advertised_description_characters
        '__MAX_LOCATION_CHARACTERS__' = [string]$limits.advertised_location_characters
    }
    $rendered = [regex]::Replace($Template, '__[A-Z0-9_]+__', [Text.RegularExpressions.MatchEvaluator]{
        param($match)
        return [string]$values[[string]$match.Value]
    })
    $bytes = [Text.Encoding]::UTF8.GetByteCount($rendered)
    if ($bytes -gt $limits.max_rendered_prompt_bytes) { throw "review-file-primary-rendered-prompt-too-large:$($limits.max_rendered_prompt_bytes)" }
    return $rendered
}

function New-ReviewHarnessProcessSpec {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$HarnessId,
        [Parameter(Mandatory)][string]$CommandName,
        [string[]]$PreArguments = @(),
        [Parameter(Mandatory)][ValidateSet('stdin', 'argument')][string]$PromptTransport,
        [Parameter(Mandatory)][string]$Prompt,
        [Parameter(Mandatory)]$Invocation,
        [AllowNull()]$Environment,
        [ValidateRange(1, 7200)][int]$TimeoutSeconds
    )
    $arguments = [Collections.Generic.List[string]]::new()
    foreach ($argument in @($PreArguments)) { $arguments.Add([string]$argument) | Out-Null }
    $stdin = $null
    if ($PromptTransport -ceq 'stdin') { $stdin = $Prompt }
    else { $arguments.Add($Prompt) | Out-Null }

    $environmentDelta = [ordered]@{}
    if ($null -ne $Environment) {
        foreach ($name in @('SPECREW_REFOCUS_DISABLE', 'SPECREW_DISABLE_EVENTS')) {
            if (($Environment -is [Collections.IDictionary]) -and $Environment.Contains($name)) { $environmentDelta[$name] = [string]$Environment[$name] }
            elseif ($Environment.PSObject.Properties[$name]) { $environmentDelta[$name] = [string]$Environment.$name }
        }
    }
    return [pscustomobject][ordered]@{
        schema_version = '1.0'; harness_id = $HarnessId; command = $CommandName; argument_list = @($arguments)
        prompt_transport = $PromptTransport; stdin_text = $stdin; working_directory = [IO.Path]::GetFullPath([string]$Invocation.snapshot_path)
        environment_delta = $environmentDelta; candidate_result_path = [IO.Path]::GetFullPath([string]$Invocation.candidate_result_path)
        deadline = [string]$Invocation.deadline; timeout_seconds = $TimeoutSeconds; result_transport = 'file-primary'; stdout_authority = $false
    }
}

function New-ReviewFilePrimaryHarnessPort {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$HarnessId,
        [Parameter(Mandatory)][string]$HostName,
        [Parameter(Mandatory)][string]$CommandName,
        [string[]]$PreArguments = @(),
        [Parameter(Mandatory)][ValidateSet('stdin', 'argument')][string]$PromptTransport,
        [Parameter(Mandatory)][string]$PromptTemplatePath,
        [ValidateRange(1, 7200)][int]$TimeoutSeconds,
        [string]$ConfiguredModel = 'configured-by-user',
        [scriptblock]$AgentInvoker,
        [scriptblock]$AvailabilityProbe
    )
    $promptPath = [IO.Path]::GetFullPath($PromptTemplatePath)
    if (-not [IO.File]::Exists($promptPath)) { throw "review-file-primary-prompt-missing:$promptPath" }
    $template = [IO.File]::ReadAllText($promptPath, [Text.UTF8Encoding]::new($false, $true))
    $templateValidation = Test-ReviewFilePrimaryPromptTemplate -Template $template
    if (-not $templateValidation.valid) { throw ('review-file-primary-prompt-invalid:' + ($templateValidation.errors -join ',')) }

    if (-not $AvailabilityProbe) { $AvailabilityProbe = { $null -ne (Get-Command -Name $CommandName -CommandType Application -ErrorAction SilentlyContinue) }.GetNewClosure() }
    if (-not $AgentInvoker) {
        $legacy = Get-Command -Name 'Invoke-ContinuousCoReviewAgentInWorktree' -CommandType Function -ErrorAction SilentlyContinue
        if ($null -ne $legacy) {
            $AgentInvoker = {
                param($worktreePath, $prompt, $timeout)
                & $legacy -WorktreePath $worktreePath -Prompt $prompt -HostName $HostName -TimeoutSeconds $timeout
            }.GetNewClosure()
        }
    }

    $renderPromptCommand = ${function:Render-ReviewFilePrimaryPrompt}
    $newProcessSpecCommand = ${function:New-ReviewHarnessProcessSpec}

    $preflight = {
        param($invocation)
        try {
            $candidate = [IO.Path]::GetFullPath([string]$invocation.candidate_result_path)
            $report = [IO.Path]::GetFullPath([string]$invocation.candidate_report_path)
            $sameParent = [IO.Path]::GetDirectoryName($candidate) -ceq [IO.Path]::GetDirectoryName($report)
            $deadline = [DateTimeOffset]::MinValue
            $deadlineValid = [DateTimeOffset]::TryParse([string]$invocation.deadline, [ref]$deadline)
            $contractValid = $true
            if (Get-Command -Name 'Test-ReviewAuthorityContractObject' -ErrorAction SilentlyContinue) {
                $contractValid = [bool](Test-ReviewAuthorityContractObject -ContractName ReviewInvocation -InputObject $invocation -ExpectedCampaignId ([string]$invocation.campaign_id) -ExpectedRunId ([string]$invocation.run_id) -ExpectedTargetDigest ([string]$invocation.target_digest)).valid
            }
            $available = [bool](& $AvailabilityProbe)
            $reason = if (-not $available) { "$HostName-unavailable" }
            elseif (-not $contractValid) { 'invocation-contract-invalid' }
            elseif (-not [IO.Directory]::Exists([string]$invocation.snapshot_path)) { 'snapshot-missing' }
            elseif (-not $sameParent -or [IO.Path]::GetFileName($candidate) -cne 'candidate.json' -or [IO.Path]::GetFileName($report) -cne 'candidate.md') { 'candidate-path-contract-invalid' }
            elseif (-not [IO.Directory]::Exists([IO.Path]::GetDirectoryName($candidate))) { 'candidate-parent-missing' }
            elseif ([IO.File]::Exists($candidate)) { 'candidate-path-preexists' }
            elseif (-not $deadlineValid) { 'deadline-invalid' }
            else { "$HostName-file-primary-ready" }
            return [pscustomobject]@{ ok = ($reason -ceq "$HostName-file-primary-ready"); reason = $reason }
        }
        catch { return [pscustomobject]@{ ok = $false; reason = ('harness-preflight-failed:' + $_.Exception.Message) } }
    }.GetNewClosure()

    $buildProcess = {
        param($invocation, $environment)
        $candidate = [IO.Path]::GetFullPath([string]$invocation.candidate_result_path)
        if ([IO.File]::Exists($candidate)) { throw "review-file-primary-candidate-preexists:$candidate" }
        $prompt = & $renderPromptCommand -Template $template -Invocation $invocation
        return & $newProcessSpecCommand -HarnessId $HarnessId -CommandName $CommandName -PreArguments $PreArguments -PromptTransport $PromptTransport -Prompt $prompt -Invocation $invocation -Environment $environment -TimeoutSeconds $TimeoutSeconds
    }.GetNewClosure()

    $invoke = {
        param($invocation, $environment)
        if (-not $AgentInvoker) { throw "review-harness-runtime-process-contract-required:$HarnessId" }
        $spec = & $buildProcess $invocation $environment
        $prompt = if ($spec.prompt_transport -ceq 'stdin') { [string]$spec.stdin_text } else { [string]$spec.argument_list[-1] }
        $agent = & $AgentInvoker ([string]$invocation.snapshot_path) $prompt $TimeoutSeconds
        $candidate = [IO.Path]::GetFullPath([string]$invocation.candidate_result_path)
        $produced = [IO.File]::Exists($candidate) -and ([IO.FileInfo]$candidate).Length -gt 0
        return [pscustomobject]@{ agent = $agent; output_activity = $produced; result_source = 'file-primary'; stdout_authority = $false }
    }.GetNewClosure()

    return [pscustomobject]@{
        id = $HarnessId; host = $HostName; configured_model = $ConfiguredModel
        contract_version = '1.0'; result_transport = 'file-primary'; stdout_authority = $false
        preflight = $preflight; build_process = $buildProcess; invoke = $invoke
    }
}

function New-ReviewUnavailableProductionHarnessPort {
    param([string]$HarnessId, [string]$HostName, [string]$Reason)
    $preflight = { param($invocation) [pscustomobject]@{ ok = $false; reason = $Reason } }.GetNewClosure()
    $build = { param($invocation, $environment) throw $Reason }.GetNewClosure()
    $invoke = { param($invocation, $environment) throw $Reason }.GetNewClosure()
    return [pscustomobject]@{ id = $HarnessId; host = $HostName; contract_version = '1.0'; result_transport = 'file-primary'; stdout_authority = $false; preflight = $preflight; build_process = $build; invoke = $invoke }
}

function New-ReviewProductionHarnessPort {
    [CmdletBinding()]
    param(
        [string]$HostName,
        [ValidateRange(1, 7200)][int]$TimeoutSeconds = 900,
        [string]$PromptTemplatePath,
        [string]$Model
    )
    if ([string]::IsNullOrWhiteSpace($HostName)) { return New-ReviewUnavailableProductionHarnessPort -HarnessId 'unselected-harness' -HostName '' -Reason 'reviewer-host-required' }
    $normalizedHost = $HostName.ToLowerInvariant()
    $definition = Get-ContinuousCoReviewProductionHarnessDefinition -HostName $normalizedHost
    if ($null -eq $definition) {
        return New-ReviewUnavailableProductionHarnessPort -HarnessId $normalizedHost -HostName $normalizedHost -Reason "production-harness-not-cataloged:$normalizedHost"
    }
    $constructorName = [string]$definition.constructor
    if ($constructorName -cnotmatch '^New-Review[A-Za-z0-9]+FilePrimaryHarnessPort$') {
        return New-ReviewUnavailableProductionHarnessPort -HarnessId ([string]$definition.harness_id) -HostName $normalizedHost -Reason "production-harness-constructor-invalid:$normalizedHost"
    }
    $constructor = Get-Command -Name $constructorName -CommandType Function -ErrorAction SilentlyContinue
    if ($null -eq $constructor) {
        return New-ReviewUnavailableProductionHarnessPort -HarnessId ([string]$definition.harness_id) -HostName $normalizedHost -Reason "production-harness-not-implemented:$normalizedHost"
    }
    $parameters = @{ TimeoutSeconds = $TimeoutSeconds }
    if (-not [string]::IsNullOrWhiteSpace($PromptTemplatePath)) { $parameters.PromptTemplatePath = $PromptTemplatePath }
    if (-not [string]::IsNullOrWhiteSpace($Model)) {
        if (-not $constructor.Parameters.ContainsKey('Model')) {
            return New-ReviewUnavailableProductionHarnessPort -HarnessId ([string]$definition.harness_id) -HostName $normalizedHost -Reason "production-harness-model-override-unsupported:$normalizedHost"
        }
        $parameters.Model = $Model
    }
    return & $constructor @parameters
}