scripts/internal/continuous-co-review/worktree-review-orchestrator.ps1

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

# iter-008 — the detached "prepare+review" orchestrator. Auto-resolves the inputs (merge-base baseline + design
# context), runs the worktree-reviewer pipeline, and writes a REAP-CONSUMABLE result (result.out = FindingsResult
# JSON, status.json = terminal status) under a run dir, then disposes the ephemeral worktree. BOTH doors — the
# navigator's fast Stop-trigger AND /specrew-review — drive THIS one pipeline (G1/G3 close here). All heavy work
# lives here, off the 20s Stop budget. See specs/197-continuous-co-review/iterations/008/design-analysis.md.

. (Join-Path $PSScriptRoot 'worktree-reviewer.ps1')

function ConvertTo-ContinuousCoReviewWorktreeIsoTimestamp {
    param([datetime]$Timestamp = [datetime]::UtcNow)
    return $Timestamp.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ', [System.Globalization.CultureInfo]::InvariantCulture)
}

function Resolve-ContinuousCoReviewWorktreeBaseline {
    # The review baseline = merge-base with trunk (the user's INCREMENT since branching, not the inception).
    # Trunk resolution is delegated to the ONE shared resolver (6-level precedence). It NEVER creates/renames/moves
    # a branch. FAIL-LOUD contract: EVERY ok=false resolver result throws with the human-facing config instruction
    # (never silently guess a branch or review everything). The empty-tree baseline is reserved for exactly ONE
    # case - the explicit successful greenfield result (a repo whose only branch is the feature branch).
    param([Parameter(Mandatory)][string]$RepoRoot, [string]$Trunk)
    $gitRoot = (& git -C $RepoRoot rev-parse --show-toplevel 2>$null).Trim()
    if ([string]::IsNullOrWhiteSpace($gitRoot)) { return $null }

    if (-not (Get-Command -Name 'Resolve-ContinuousCoReviewTrunkRef' -ErrorAction SilentlyContinue)) {
        $resolverPath = Join-Path $PSScriptRoot 'co-review-trunk-resolver.ps1'
        if (Test-Path -LiteralPath $resolverPath -PathType Leaf) { . $resolverPath }
    }
    $resolvedTrunk = Resolve-ContinuousCoReviewTrunkRef -RepoRoot $gitRoot -Trunk $Trunk

    # FAIL-LOUD on EVERY ok=false (ambiguous, explicit-trunk-unresolvable, no-commit-repo): surface the resolver's
    # configuration instruction rather than silently guessing or reviewing everything.
    if (-not $resolvedTrunk.ok) {
        throw ("[continuous-co-review] {0}" -f $resolvedTrunk.message)
    }

    # The ONLY empty-tree path: the explicit successful GREENFIELD result - source='greenfield' AND a null
    # trunk_ref TOGETHER (exactly the documented one case). Reviews the whole feature's source instead of failing
    # to run - the root cause of the first real e2e producing zero co-review evidence. The strip/digest list still
    # excludes .specrew/.specify machinery, so the empty-tree baseline reviews source only, not scaffolding.
    if ($resolvedTrunk.source -eq 'greenfield' -and [string]::IsNullOrWhiteSpace([string]$resolvedTrunk.trunk_ref)) {
        return '4b825dc642cb6eb9a060e54bf8d69288fbee4904'
    }

    # Any OTHER ok=true result with an empty trunk_ref is a resolver invariant break (a non-greenfield ok result
    # must carry a ref): fail loudly rather than silently empty-tree or merge-base against nothing.
    if ([string]::IsNullOrWhiteSpace([string]$resolvedTrunk.trunk_ref)) {
        throw ("[continuous-co-review] The trunk resolver returned an empty trunk under source '{0}' (a non-greenfield ok result must carry a ref); refusing to guess a review baseline. Set 'co_review_trunk: <branch>' in .specrew/config.yml." -f $resolvedTrunk.source)
    }

    $mb = (& git -C $gitRoot merge-base HEAD $resolvedTrunk.trunk_ref 2>$null)
    if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($mb)) {
        # A trunk resolved but shares NO history with HEAD (unrelated/grafted histories): fail loudly rather than
        # silently reviewing everything against an unrelated trunk. Empty-tree is reserved for greenfield only.
        throw ("[continuous-co-review] The resolved trunk '{0}' shares no history with HEAD (unrelated histories); a review baseline cannot be computed. Set 'co_review_trunk: <branch>' in .specrew/config.yml to the correct base branch." -f $resolvedTrunk.trunk_ref)
    }
    return ([string]$mb).Trim()
}

function Resolve-ContinuousCoReviewReviewerHost {
    # Select the reviewer host: code-writer-INDEPENDENT + AUTHORIZED (reviewer-hosts.json), via the legacy policy
    # (reused, NOT reinvented). Returns @{ host; model } or $null (no authorized host -> the caller fails-soft with
    # a stated reason). Lazy-loads the CCR engine (_load) if the catalog/policy aren't in scope (the detached path
    # dot-sources only the orchestrator). This is what makes the worktree reviewer host-NEUTRAL + authorized, not
    # claude-pinned.
    param([Parameter(Mandatory)][string]$RepoRoot, [string]$CodeWriterHost, [string]$RequestedHost)
    # F-198 FR-023: when no explicit code-writer host was passed, resolve it from the session
    # env the same way --list-hosts does (SPECREW_HOST -> SPECREW_ACTIVE_HOST) and RECORD the
    # provenance: an env-derived independence label is the session's own declaration, not a
    # human assertion, and the audit trail must distinguish them (independence_source:
    # flag | env | unverified; the fail-closed treatment of 'unverified' is unchanged).
    $codeWriterSource = 'unverified'
    if (-not [string]::IsNullOrWhiteSpace($CodeWriterHost)) { $codeWriterSource = 'flag' }
    elseif (-not [string]::IsNullOrWhiteSpace($env:SPECREW_HOST)) { $CodeWriterHost = $env:SPECREW_HOST; $codeWriterSource = 'env' }
    elseif (-not [string]::IsNullOrWhiteSpace($env:SPECREW_ACTIVE_HOST)) { $CodeWriterHost = $env:SPECREW_ACTIVE_HOST; $codeWriterSource = 'env' }
    if (-not (Get-Command 'Select-ContinuousCoReviewReviewerCandidate' -ErrorAction SilentlyContinue)) {
        $loadPath = Join-Path $PSScriptRoot '_load.ps1'
        if (Test-Path -LiteralPath $loadPath -PathType Leaf) { try { . $loadPath } catch { $null = $_ } }
    }
    if (-not (Get-Command 'Get-ContinuousCoReviewReviewerHostCatalog' -ErrorAction SilentlyContinue) -or -not (Get-Command 'Select-ContinuousCoReviewReviewerCandidate' -ErrorAction SilentlyContinue)) { return $null }
    try {
        # Same load path as the legacy navigator (continuous-co-review-navigator.ps1:849-865): the persisted
        # human-authorized config -> catalog -> independent+authorized candidate. T093: an explicit
        # -RequestedHost restricts the selection (honour-or-surface); the returned independence label
        # ('independent' | 'same-host' | 'unverified') is the D4 gate's evidence dimension.
        $reviewerConfig = $null
        $reviewerHostsPath = Join-Path $RepoRoot '.specrew/reviewer-hosts.json'
        if (Test-Path -LiteralPath $reviewerHostsPath -PathType Leaf) {
            try { $reviewerConfig = (Get-Content -LiteralPath $reviewerHostsPath -Raw -Encoding UTF8 | ConvertFrom-Json -Depth 100) } catch { $reviewerConfig = $null }
        }
        $catalog = Get-ContinuousCoReviewReviewerHostCatalog -Configuration $reviewerConfig
        $cand = Select-ContinuousCoReviewReviewerCandidate -Catalog $catalog -CodeWriterHost $CodeWriterHost -RequestedHost $RequestedHost
        if ($null -eq $cand -or [string]::IsNullOrWhiteSpace([string]$cand.host)) { return $null }
        $indep = if ($cand.PSObject.Properties['independence']) { [string]$cand.independence } else { 'unverified' }
        $selReason = if ($cand.PSObject.Properties['selection_reason']) { [string]$cand.selection_reason } else { '' }
        return [pscustomobject]@{ host = [string]$cand.host; model = [string]$cand.model; independence = $indep; selection_reason = $selReason; independence_source = $codeWriterSource }
    }
    catch { return $null }
}

function Get-ContinuousCoReviewMaxRounds {
    # co_review_max_rounds (config, default 2). The review->fix->re-review ceiling before escalation.
    param([Parameter(Mandatory)][string]$RepoRoot)
    $cfg = Join-Path $RepoRoot '.specrew/config.yml'
    if (Test-Path -LiteralPath $cfg -PathType Leaf) {
        foreach ($line in (Get-Content -LiteralPath $cfg -Encoding UTF8)) {
            if ($line -match '^\s*co_review_max_rounds\s*:\s*(\d+)') { $n = [int]$Matches[1]; if ($n -ge 1) { return $n } }
        }
    }
    return 2
}

function Get-ContinuousCoReviewRoundStatePath { param([Parameter(Mandatory)][string]$RepoRoot) return (Join-Path $RepoRoot '.specrew/runtime/co-review-round-state.json') }

function Get-ContinuousCoReviewRoundState {
    param([Parameter(Mandatory)][string]$RepoRoot)
    $p = Get-ContinuousCoReviewRoundStatePath -RepoRoot $RepoRoot
    if (Test-Path -LiteralPath $p -PathType Leaf) { try { return (Get-Content -LiteralPath $p -Raw -Encoding UTF8 | ConvertFrom-Json) } catch { return $null } }
    return $null
}

function Set-ContinuousCoReviewRoundState {
    param([Parameter(Mandatory)][string]$RepoRoot, [string[]]$ChangedPaths, [int]$Round, [bool]$Blocking, [string]$Findings)
    $p = Get-ContinuousCoReviewRoundStatePath -RepoRoot $RepoRoot
    try {
        $dir = Split-Path -Parent $p; if ($dir -and -not (Test-Path -LiteralPath $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null }
        # T096/FR-038: PRESERVE an un-consumed remediation choice across the per-run rewrite (the
        # human may record it between runs; only the consumer clears it). T020/FR-019: likewise
        # PRESERVE the resolved-against-disk disposition trail - it is the durable record the halt
        # text reads to show resolved-vs-open, and it must not be wiped by a per-run status write.
        $remediation = $null
        $dispositions = @()
        try {
            if (Test-Path -LiteralPath $p -PathType Leaf) {
                $prior = Get-Content -LiteralPath $p -Raw -Encoding UTF8 | ConvertFrom-Json
                if ($null -ne $prior -and ($prior.PSObject.Properties.Name -contains 'remediation')) { $remediation = $prior.remediation }
                if ($null -ne $prior -and ($prior.PSObject.Properties.Name -contains 'dispositions') -and $null -ne $prior.dispositions) { $dispositions = @($prior.dispositions) }
            }
        }
        catch { $remediation = $null; $dispositions = @() }
        ([pscustomobject]@{ changed_paths = @($ChangedPaths); round = $Round; blocking = $Blocking; findings = $Findings; remediation = $remediation; dispositions = $dispositions } | ConvertTo-Json -Depth 8 -Compress) | Set-Content -LiteralPath $p -Encoding UTF8
    }
    catch { $null = $_ }
}

function Add-ContinuousCoReviewRoundDisposition {
    # T020 (FR-019): append a disposition (e.g. a failed-invocation record) to the round-state trail
    # WITHOUT disturbing the round/blocking/findings fields. The trail is the durable accounting record
    # the halt text and audits read - a failed invocation never disappears from it.
    param([Parameter(Mandatory)][string]$RepoRoot, [Parameter(Mandatory)][object]$Disposition)
    $resolved = (Resolve-Path -LiteralPath $RepoRoot).Path
    $p = Get-ContinuousCoReviewRoundStatePath -RepoRoot $resolved
    $state = Get-ContinuousCoReviewRoundState -RepoRoot $resolved
    if ($null -eq $state) { $state = [pscustomobject]@{ changed_paths = @(); round = 0; blocking = $false; findings = $null; remediation = $null; dispositions = @() } }
    $disp = @()
    if (($state.PSObject.Properties.Name -contains 'dispositions') -and $null -ne $state.dispositions) { $disp = @($state.dispositions) }
    $disp += $Disposition
    $state | Add-Member -NotePropertyName 'dispositions' -NotePropertyValue $disp -Force
    ($state | ConvertTo-Json -Depth 8 -Compress) | Set-Content -LiteralPath $p -Encoding UTF8
    return $Disposition
}

function Get-ContinuousCoReviewRoundSpendClass {
    # T020 (F-198 FR-018/FR-019, before-implement send-back): separate the TWO budgets a review run
    # touches - PROVIDER SPEND (actual model/API cost) and the REVIEW-ROUND ALLOWANCE (the autonomous
    # ceiling). Classifies one run outcome:
    # 'preflight-failed' -> a required input (e.g. .review/changes.diff) was missing BEFORE the
    # model was invoked: an INFRASTRUCTURE failure that consumes NEITHER
    # provider budget NOR a round-allowance slot (it prevented the wasteful
    # invocation the field da2bc5cc round suffered).
    # 'invoked-reviewed' -> the model was invoked and produced a review: provider spend recorded,
    # the round counts.
    # 'invoked-failed' -> the model WAS invoked but produced no valid review: provider spend IS
    # recorded (real cost), AND the round counts with a failed-invocation
    # disposition - it never disappears from accounting.
    # Returns @{ class; consumes_round; records_provider_spend; reason }.
    param(
        [Parameter(Mandatory)][bool]$InputMaterialized,
        [Parameter(Mandatory)][bool]$ModelInvoked,
        [Parameter(Mandatory)][bool]$ProducedReview
    )
    if (-not $InputMaterialized -and -not $ModelInvoked) {
        return [pscustomobject]@{ class = 'preflight-failed'; consumes_round = $false; records_provider_spend = $false; reason = 'required review input was not materialized; the model was never invoked (infrastructure failure)' }
    }
    if ($ModelInvoked -and $ProducedReview) {
        return [pscustomobject]@{ class = 'invoked-reviewed'; consumes_round = $true; records_provider_spend = $true; reason = 'the reviewer was invoked and produced a review' }
    }
    if ($ModelInvoked -and -not $ProducedReview) {
        return [pscustomobject]@{ class = 'invoked-failed'; consumes_round = $true; records_provider_spend = $true; reason = 'the reviewer was invoked but produced no valid review (provider spend still incurred; the round is counted with a failed-invocation disposition)' }
    }
    # Input materialized but the model was (intentionally) not invoked, and no review: treat as a
    # preflight-class no-op that consumes neither (defensive; no spend occurred).
    return [pscustomobject]@{ class = 'preflight-failed'; consumes_round = $false; records_provider_spend = $false; reason = 'the model was not invoked; no provider spend or round consumed' }
}

function Set-ContinuousCoReviewFindingResolvedAgainstDisk {
    # T020 (F-198 FR-018/FR-019; SPLIT per maintainer ruling 2026-07-12, DRIFT-198-I003-005): a
    # RESOLVED-AGAINST-DISK disposition. When a held blocking finding has been FIXED and the fix is
    # committed IN THE REVIEWED LINEAGE (an ancestor of HEAD), this records the resolution and CLEARS
    # the sticky blocking FINDING + change-set lineage - so the finding can NEITHER re-escalate NOR keep
    # climbing the ceiling on its file overlap. It DELIBERATELY PRESERVES the number of rounds already
    # spent: the ceiling is an AI-usage SPEND allowance where EVERY round counts (spec FR-019), so
    # resolving a finding NEVER implicitly replenishes review allowance. Replenishing/extending the
    # allowance is a SEPARATE, explicit human-approved action (Set-ContinuousCoReviewAllowanceReset).
    # (Earlier this reset round=0 and unintentionally replenished allowance - DRIFT-198-I003-005.) This is
    # NOT override-block (which waves a DEGRADED block through): the finding is genuinely resolved, so a
    # fix-evidence commit that is an ANCESTOR OF HEAD is REQUIRED - an unverifiable/absent ref is refused,
    # so a bare 'resolved' claim can never clear the latch (no false-green door). The rounds already spent
    # stay recorded in the round-state AND the disposition trail.
    param(
        [Parameter(Mandatory)][string]$RepoRoot,
        [Parameter(Mandatory)][string]$FixEvidenceRef,
        [string]$AuthorizedBy,
        [datetime]$Now = [datetime]::UtcNow
    )
    $resolved = (Resolve-Path -LiteralPath $RepoRoot).Path
    $commit = (& git -C $resolved rev-parse --verify "$FixEvidenceRef^{commit}" 2>$null)
    if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($commit)) {
        throw "resolved-against-disk needs a real fix-evidence commit; '$FixEvidenceRef' does not resolve to a commit (a bare 'resolved' claim cannot clear the review latch)."
    }
    $commit = $commit.Trim()
    & git -C $resolved merge-base --is-ancestor $commit HEAD 2>$null | Out-Null
    if ($LASTEXITCODE -ne 0) {
        throw "resolved-against-disk fix evidence '$FixEvidenceRef' is not an ancestor of HEAD: the fix is not in the reviewed tree, so the finding is NOT resolved-against-disk."
    }
    $prior = Get-ContinuousCoReviewRoundState -RepoRoot $resolved
    if ($null -eq $prior) { return $null }   # nothing latched to resolve
    if ([string]::IsNullOrWhiteSpace($AuthorizedBy)) {
        $AuthorizedBy = (& git -C $resolved config user.name 2>$null)
        if ([string]::IsNullOrWhiteSpace([string]$AuthorizedBy)) { $AuthorizedBy = 'human' }
    }
    $findingId = ''
    try { $findingId = [string](($prior.findings | ConvertFrom-Json).findings[0].finding_id) } catch { $findingId = '' }
    $disposition = [pscustomobject][ordered]@{
        state                          = 'resolved-against-disk'
        finding_id                     = $findingId
        fix_evidence_ref               = $commit
        authorized_by                  = ([string]$AuthorizedBy).Trim()
        recorded_at                    = $Now.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ', [System.Globalization.CultureInfo]::InvariantCulture)
        rounds_spent_before_resolution = [int]$prior.round
    }
    $dispositions = @()
    if (($prior.PSObject.Properties.Name -contains 'dispositions') -and $null -ne $prior.dispositions) { $dispositions = @($prior.dispositions) }
    $dispositions += $disposition
    # Clear the FINDING lineage: blocking=false, findings=null, changed_paths reset - the resolved
    # finding no longer climbs the ceiling nor re-surfaces. PRESERVE round: the spent allowance is NOT
    # replenished here (that is Set-ContinuousCoReviewAllowanceReset's job). The remediation carrier and
    # the (now-appended) disposition trail are preserved.
    $p = Get-ContinuousCoReviewRoundStatePath -RepoRoot $resolved
    $remediation = if (($prior.PSObject.Properties.Name -contains 'remediation')) { $prior.remediation } else { $null }
    ([pscustomobject][ordered]@{ changed_paths = @(); round = [int]$prior.round; blocking = $false; findings = $null; remediation = $remediation; dispositions = $dispositions } | ConvertTo-Json -Depth 8 -Compress) | Set-Content -LiteralPath $p -Encoding UTF8
    return $disposition
}

function Set-ContinuousCoReviewAllowanceReset {
    # T020 SPLIT (maintainer ruling 2026-07-12, DRIFT-198-I003-005): the SEPARATE, explicit human-approved
    # action that REPLENISHES the review-round SPEND ALLOWANCE. The ceiling is an AI-usage spend allowance
    # (spec FR-019); resolving a finding NEVER replenishes it - only THIS does, and only on a deliberate
    # human decision. It resets the spent round count to 0, records WHO authorized it, WHEN, and WHY, and
    # LEAVES the resolved-finding evidence (dispositions) + any open finding/blocking lineage INTACT (it
    # grants budget, it does not resolve findings). TRUST BOUNDARY: construct only from a genuinely
    # human-typed/approved command. (A ceiling EXTENSION was removed: the runtime ceiling reads ONLY the
    # configured .specrew/config.yml, so a round-state max_rounds override would be a DEAD, misleading audit
    # claim - advisory f1, authoritative review 20260712T215431762. allowance-reset only does the ENFORCED
    # round=0 replenish.)
    param(
        [Parameter(Mandatory)][string]$RepoRoot,
        [string]$AuthorizedBy,
        [string]$Reason,
        [datetime]$Now = [datetime]::UtcNow
    )
    $resolved = (Resolve-Path -LiteralPath $RepoRoot).Path
    if ([string]::IsNullOrWhiteSpace($AuthorizedBy)) {
        $AuthorizedBy = (& git -C $resolved config user.name 2>$null)
        if ([string]::IsNullOrWhiteSpace([string]$AuthorizedBy)) { $AuthorizedBy = [string]$env:USERNAME }
        if ([string]::IsNullOrWhiteSpace([string]$AuthorizedBy)) { $AuthorizedBy = 'human' }
    }
    $prior = Get-ContinuousCoReviewRoundState -RepoRoot $resolved
    if ($null -eq $prior) { $prior = [pscustomobject]@{ changed_paths = @(); round = 0; blocking = $false; findings = $null } }
    $priorRound = [int]$prior.round
    $disposition = [pscustomobject][ordered]@{
        state          = 'allowance-reset'
        authorized_by  = ([string]$AuthorizedBy).Trim()
        recorded_at    = $Now.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ', [System.Globalization.CultureInfo]::InvariantCulture)
        reason         = if ([string]::IsNullOrWhiteSpace($Reason)) { $null } else { ([string]$Reason).Trim() }
        previous_round = $priorRound
        new_round      = 0
    }
    $dispositions = @()
    if (($prior.PSObject.Properties.Name -contains 'dispositions') -and $null -ne $prior.dispositions) { $dispositions = @($prior.dispositions) }
    $dispositions += $disposition
    # REPLENISH: round=0. PRESERVE the finding lineage (blocking/findings/changed_paths) + all evidence -
    # granting budget is orthogonal to resolving findings.
    $priorBlocking = ($prior.PSObject.Properties['blocking'] -and [bool]$prior.blocking)
    $priorFindings = if ($prior.PSObject.Properties['findings']) { [string]$prior.findings } else { $null }
    $priorChanged = if ($prior.PSObject.Properties['changed_paths']) { @($prior.changed_paths) } else { @() }
    $remediation = if (($prior.PSObject.Properties.Name -contains 'remediation')) { $prior.remediation } else { $null }
    $state = [ordered]@{ changed_paths = $priorChanged; round = 0; blocking = $priorBlocking; findings = $priorFindings; remediation = $remediation; dispositions = $dispositions }
    $p = Get-ContinuousCoReviewRoundStatePath -RepoRoot $resolved
    ([pscustomobject]$state | ConvertTo-Json -Depth 8 -Compress) | Set-Content -LiteralPath $p -Encoding UTF8
    return $disposition
}

function Set-ContinuousCoReviewRemediationChoice {
    <#
        T096/FR-038 (iter-009 D6/R6): record the human's remediation choice onto the round-state so
        the NEXT run applies it (the menu's carrier). Choices:
          more-time (+TimeoutSeconds) | different-host (+HostName) | narrow-scope (+Scope) |
          accept-partial (+RunId +Reason: records the T094 degraded ack immediately) |
          override-block (+RunId +Reason: D5 - only a DEGRADED run's block is overridable).
        TRUST BOUNDARY: construct only from a genuinely human-typed command
        (`specrew review --remediate ...`) or a captured human verdict.
    #>

    param(
        [Parameter(Mandatory)][string]$RepoRoot,
        [Parameter(Mandatory)][ValidateSet('more-time', 'different-host', 'narrow-scope', 'accept-partial', 'override-block', 'resolved-against-disk', 'allowance-reset')][string]$Choice,
        [int]$TimeoutSeconds = 0,
        [string]$HostName,
        [string]$Scope,
        [string]$RunId,
        [string]$Reason,
        [string]$FixEvidenceRef,
        [string]$AuthorizedBy,
        [datetime]$Now = [datetime]::UtcNow
    )
    $resolved = (Resolve-Path -LiteralPath $RepoRoot).Path
    if ([string]::IsNullOrWhiteSpace($AuthorizedBy)) {
        $AuthorizedBy = (& git -C $resolved config user.name 2>$null)
        if ([string]::IsNullOrWhiteSpace([string]$AuthorizedBy)) { $AuthorizedBy = [string]$env:USERNAME }
        if ([string]::IsNullOrWhiteSpace([string]$AuthorizedBy)) { $AuthorizedBy = 'human' }
    }
    $AuthorizedBy = ([string]$AuthorizedBy).Trim()

    switch ($Choice) {
        'different-host' { if ([string]::IsNullOrWhiteSpace($HostName)) { throw "remediation 'different-host' needs --host <name>." } }
        'narrow-scope' {
            if ([string]::IsNullOrWhiteSpace($Scope)) { throw "remediation 'narrow-scope' needs --scope <code|process|path:<p>|function:<name>>." }
            if ($Scope -notmatch '^(code|process|path:.+|function:.+)$') { throw "remediation scope '$Scope' is not one of code | process | path:<p> | function:<name>." }
        }
        'accept-partial' {
            if ([string]::IsNullOrWhiteSpace($RunId) -or [string]::IsNullOrWhiteSpace($Reason)) { throw "remediation 'accept-partial' needs --run-id <id> and --ack-reason '<why>'." }
            # Accepting the partial IS the T094 first-class ack - record it immediately (no rerun).
            if (-not (Get-Command -Name 'Add-ContinuousCoReviewDegradedAck' -ErrorAction SilentlyContinue)) {
                $lp = Join-Path $PSScriptRoot '_load.ps1'; if (Test-Path -LiteralPath $lp -PathType Leaf) { . $lp }
            }
            $null = Add-ContinuousCoReviewDegradedAck -RepoRoot $resolved -RunId $RunId -AuthorizedBy $AuthorizedBy -Rationale $Reason -Now $Now
        }
        'override-block' {
            if ([string]::IsNullOrWhiteSpace($RunId) -or [string]::IsNullOrWhiteSpace($Reason)) { throw "remediation 'override-block' needs --run-id <id> and --ack-reason '<why>'." }
            # D5: ONLY a DEGRADED run's block is overridable - a full+independent review's blocking
            # finding must be addressed, not waved through. Verify from the run's terminal status.
            $rdir = Join-Path $resolved ".specrew/review/pending/$RunId"
            $stPath = Join-Path $rdir 'status.json'
            $labels = $null
            if (Test-Path -LiteralPath $stPath -PathType Leaf) {
                try {
                    $st = Get-Content -LiteralPath $stPath -Raw -Encoding UTF8 | ConvertFrom-Json
                    $comp = if (($st.PSObject.Properties.Name -contains 'completeness') -and -not [string]::IsNullOrWhiteSpace([string]$st.completeness)) { [string]$st.completeness } else { 'full' }
                    $indep = if (($st.PSObject.Properties.Name -contains 'reviewer_independence') -and -not [string]::IsNullOrWhiteSpace([string]$st.reviewer_independence)) { [string]$st.reviewer_independence } else { 'unverified' }
                    $labels = [pscustomobject]@{ completeness = $comp; independence = $indep }
                }
                catch { $labels = $null }
            }
            if ($null -eq $labels) { throw "remediation 'override-block': run '$RunId' has no readable terminal status - cannot verify the review was degraded, so its block is NOT overridable (address the finding or re-run)." }
            if ($labels.completeness -eq 'full' -and $labels.independence -eq 'independent') {
                throw "remediation 'override-block': run '$RunId' was a FULL INDEPENDENT review - its blocking finding must be addressed (or re-reviewed), never overridden (D5 allows overriding DEGRADED blocks only)."
            }
            # Record the durable override + clear the sticky blocking round-state so the loop unblocks.
            $ovDir = Join-Path $resolved ".specrew/review/inline/$RunId"
            New-Item -ItemType Directory -Path $ovDir -Force | Out-Null
            ([pscustomobject][ordered]@{
                schema_version = '1.0'; run_id = $RunId; authorized_by = $AuthorizedBy; rationale = $Reason
                evidence_labels = $labels
                overridden_at = $Now.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ', [System.Globalization.CultureInfo]::InvariantCulture)
            } | ConvertTo-Json -Depth 8) | Set-Content -LiteralPath (Join-Path $ovDir 'degraded-block-override.json') -Encoding UTF8 -NoNewline
            $prior = Get-ContinuousCoReviewRoundState -RepoRoot $resolved
            if ($null -ne $prior) {
                Set-ContinuousCoReviewRoundState -RepoRoot $resolved -ChangedPaths @($prior.changed_paths) -Round ([int]$prior.round) -Blocking $false -Findings ([string]$prior.findings)
            }
        }
        'resolved-against-disk' {
            # T020: the held blocking finding has been FIXED and the fix is committed IN HEAD's
            # lineage. Distinct from override-block (which waves a DEGRADED block through): this
            # requires committed fix evidence (an ancestor of HEAD) and clears the latch + resets the
            # round so the resolved finding can neither re-escalate nor keep consuming the allowance.
            if ([string]::IsNullOrWhiteSpace($FixEvidenceRef)) { throw "remediation 'resolved-against-disk' needs --fix-evidence-ref <commit> (the commit that resolves the finding)." }
            $null = Set-ContinuousCoReviewFindingResolvedAgainstDisk -RepoRoot $resolved -FixEvidenceRef $FixEvidenceRef -AuthorizedBy $AuthorizedBy -Now $Now
        }
        'allowance-reset' {
            # T020 SPLIT (DRIFT-198-I003-005): the SEPARATE, explicit human-approved REPLENISH of the
            # review-round spend allowance (resolved-against-disk no longer does this implicitly). Requires
            # --ack-reason so the audit records WHY the human granted more review budget.
            if ([string]::IsNullOrWhiteSpace($Reason)) { throw "remediation 'allowance-reset' needs --ack-reason '<why>' (records the human-approved allowance reset)." }
            $null = Set-ContinuousCoReviewAllowanceReset -RepoRoot $resolved -AuthorizedBy $AuthorizedBy -Reason $Reason -Now $Now
        }
    }

    $remediation = [pscustomobject][ordered]@{
        choice          = $Choice
        timeout_seconds = if ($TimeoutSeconds -gt 0) { $TimeoutSeconds } else { $null }
        host            = if ([string]::IsNullOrWhiteSpace($HostName)) { $null } else { $HostName }
        scope           = if ([string]::IsNullOrWhiteSpace($Scope)) { $null } else { $Scope }
        run_id          = if ([string]::IsNullOrWhiteSpace($RunId)) { $null } else { $RunId }
        reason          = if ([string]::IsNullOrWhiteSpace($Reason)) { $null } else { $Reason }
        authorized_by   = $AuthorizedBy
        recorded_at     = $Now.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ', [System.Globalization.CultureInfo]::InvariantCulture)
    }
    # accept-partial / override-block act immediately (above); the rerun-shaping choices ride the
    # round-state to the next run.
    if ($Choice -in @('more-time', 'different-host', 'narrow-scope')) {
        $p = Get-ContinuousCoReviewRoundStatePath -RepoRoot $resolved
        $dir = Split-Path -Parent $p; if ($dir -and -not (Test-Path -LiteralPath $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null }
        $state = Get-ContinuousCoReviewRoundState -RepoRoot $resolved
        if ($null -eq $state) { $state = [pscustomobject]@{ changed_paths = @(); round = 0; blocking = $false; findings = $null } }
        $state | Add-Member -NotePropertyName 'remediation' -NotePropertyValue $remediation -Force
        ($state | ConvertTo-Json -Depth 8 -Compress) | Set-Content -LiteralPath $p -Encoding UTF8
    }
    return $remediation
}

function Read-ContinuousCoReviewRemediationChoice {
    # ONE-SHOT consumer (T096): return the pending remediation (or $null) and CLEAR it from the
    # round-state - a human choice applies to exactly one rerun, never silently forever.
    param([Parameter(Mandatory)][string]$RepoRoot)
    $state = Get-ContinuousCoReviewRoundState -RepoRoot $RepoRoot
    if ($null -eq $state -or -not ($state.PSObject.Properties.Name -contains 'remediation') -or $null -eq $state.remediation) { return $null }
    $choice = $state.remediation
    try {
        $state.remediation = $null
        $p = Get-ContinuousCoReviewRoundStatePath -RepoRoot $RepoRoot
        ($state | ConvertTo-Json -Depth 8 -Compress) | Set-Content -LiteralPath $p -Encoding UTF8
    }
    catch { $null = $_ }
    return $choice
}

function Test-ContinuousCoReviewPathLineageOverlap {
    # Same review LINEAGE = the current change-set overlaps the prior round's (a fix attempt on the SAME area) -
    # NOT merely "the prior was blocking" (which conflates unrelated checkpoints -> spurious escalation + irrelevant
    # prior findings). Overlap -> increment + thread prior findings; no overlap -> a new checkpoint (round 1).
    param([string[]]$Current, [string[]]$Prior)
    if (-not $Current -or -not $Prior -or @($Current).Count -eq 0 -or @($Prior).Count -eq 0) { return $false }
    $set = [System.Collections.Generic.HashSet[string]]::new([string[]]@($Prior), [System.StringComparer]::OrdinalIgnoreCase)
    foreach ($c in @($Current)) { if ($set.Contains([string]$c)) { return $true } }
    return $false
}

function Get-ContinuousCoReviewFindingsJson {
    # Robustly extract the FindingsResult JSON from a free-form agentic reviewer's stdout. The reviewer is told to
    # output ONLY the JSON, but a non-deterministic agent may narrate AROUND it with prose containing braces
    # (`if (x) { ... }`), so a naive first-brace..last-brace span can capture non-JSON and false-fail the run. Try,
    # in order: a ```json fence, the whole span, then balanced {...} objects scanned from the END (the prompt asks
    # for the JSON last). Accept the first candidate that PARSES and carries a `findings` property (the contract
    # marker). Returns the JSON string, or $null if no valid FindingsResult is present.
    param([Parameter(Mandatory)][AllowEmptyString()][string]$Raw)
    if ([string]::IsNullOrWhiteSpace($Raw)) { return $null }
    $candidates = New-Object System.Collections.Generic.List[string]
    $fence = [regex]::Match($Raw, '(?s)```(?:json)?\s*(\{.*\})\s*```')
    if ($fence.Success) { [void]$candidates.Add($fence.Groups[1].Value) }
    $s = $Raw.IndexOf('{'); $e = $Raw.LastIndexOf('}')
    if ($s -ge 0 -and $e -gt $s) { [void]$candidates.Add($Raw.Substring($s, $e - $s + 1)) }
    for ($i = $Raw.Length - 1; $i -ge 0 -and $candidates.Count -lt 8; $i--) {
        if ($Raw[$i] -ne '}') { continue }
        $depth = 0
        for ($j = $i; $j -ge 0; $j--) {
            if ($Raw[$j] -eq '}') { $depth++ }
            elseif ($Raw[$j] -eq '{') { $depth--; if ($depth -eq 0) { [void]$candidates.Add($Raw.Substring($j, $i - $j + 1)); break } }
        }
    }
    foreach ($cand in $candidates) {
        if ([string]::IsNullOrWhiteSpace($cand)) { continue }
        try { $o = $cand | ConvertFrom-Json -Depth 100; if ($null -ne $o -and $o.PSObject.Properties['findings']) { return $cand } } catch { $null = $_ }
    }
    return $null
}

function Invoke-ContinuousCoReviewWorktreeReviewRun {
    # The full detached run: auto-resolve → materialize stripped worktree + .review/ → agentic review → write a
    # reap-consumable result under $RunDir, then dispose. Returns the terminal status object.
    param(
        [Parameter(Mandatory)][string]$RepoRoot,
        [Parameter(Mandatory)][string]$RunDir,
        [Parameter(Mandatory)][string]$RunId,
        [string]$BaselineRef,
        [string[]]$DesignContextFiles,
        [string]$CodeWriterHost,
        [string]$RequestedHost,
        [int]$TimeoutSeconds = 900
    )
    New-Item -ItemType Directory -Path $RunDir -Force | Out-Null
    $resultOut = Join-Path $RunDir 'result.out'
    $statusPath = Join-Path $RunDir 'status.json'
    $startedAt = [datetime]::UtcNow
    $runTimer = [System.Diagnostics.Stopwatch]::StartNew()

    # T096/FR-038 (iter-009 D6/R6): consume a pending human remediation choice (ONE-SHOT) and shape
    # THIS run with it - more time (budget), different host (selection), narrow scope (the reviewer's
    # human-directed scope). accept-partial/override-block acted immediately at record time.
    $remediation = Read-ContinuousCoReviewRemediationChoice -RepoRoot $RepoRoot
    $humanScope = $null
    $remediationApplied = $null
    $designContextEmpty = $false   # set for real at the design-context-resolution phase (f1)
    if ($null -ne $remediation) {
        $remediationApplied = [string]$remediation.choice
        switch ($remediationApplied) {
            'more-time' {
                $req = 0
                try { if (($remediation.PSObject.Properties.Name -contains 'timeout_seconds') -and $null -ne $remediation.timeout_seconds) { $req = [int]$remediation.timeout_seconds } } catch { $req = 0 }
                if ($req -le 0) { $req = $TimeoutSeconds * 2 }
                $TimeoutSeconds = [math]::Max($TimeoutSeconds, $req)
            }
            'different-host' {
                if (($remediation.PSObject.Properties.Name -contains 'host') -and -not [string]::IsNullOrWhiteSpace([string]$remediation.host)) { $RequestedHost = [string]$remediation.host }
            }
            'narrow-scope' {
                if (($remediation.PSObject.Properties.Name -contains 'scope') -and -not [string]::IsNullOrWhiteSpace([string]$remediation.scope)) { $humanScope = [string]$remediation.scope }
            }
        }
    }

    $phaseTimings = [ordered]@{}
    $phaseTimers = @{}
    $currentPhase = 'initializing'
    $softBudgetSeconds = [math]::Max(60, [math]::Min([int]($TimeoutSeconds * 0.35), 300))
    # T092/R2 (FR-034): the generous-budget bump is computed AFTER worktree materialization (once the diff size is
    # known). Tracked here so every status write records the default vs the effective (possibly bumped) budget.
    $budgetDefaultSeconds = $TimeoutSeconds; $budgetBumped = $false
    $budgetPolicy = 'Use implementer validation evidence first. Spend reviewer runtime where it materially changes confidence; targeted reruns are preferred over broad suites unless broad verification is justified by risk.'
    $recordPhaseStart = {
        param([string]$Name)
        if ([string]::IsNullOrWhiteSpace($Name)) { return }
        $phaseTimers[$Name] = [System.Diagnostics.Stopwatch]::StartNew()
    }
    $recordPhaseEnd = {
        param([string]$Name)
        if ([string]::IsNullOrWhiteSpace($Name)) { return }
        if ($phaseTimers.Contains($Name)) {
            $phaseTimers[$Name].Stop()
            $phaseTimings[$Name] = [math]::Round($phaseTimers[$Name].Elapsed.TotalSeconds, 3)
            $phaseTimers.Remove($Name)
        }
    }
    $writeStatus = {
        param([string]$St, [hashtable]$Extra)
        $obj = [ordered]@{
            schema_version = '1.0'
            run_id         = $RunId
            status         = $St
            phase          = $currentPhase
            started_at     = ConvertTo-ContinuousCoReviewWorktreeIsoTimestamp -Timestamp $startedAt
            updated_at     = ConvertTo-ContinuousCoReviewWorktreeIsoTimestamp
            elapsed_seconds = [math]::Round($runTimer.Elapsed.TotalSeconds, 3)
            timeout_seconds = $TimeoutSeconds
            soft_budget_seconds = $softBudgetSeconds
            budget_policy = $budgetPolicy
            budget_default_seconds = $budgetDefaultSeconds
            budget_bumped = $budgetBumped
            artifacts = [ordered]@{
                run_dir     = $RunDir
                result_out  = $resultOut
                status_json = $statusPath
            }
            remediation_applied = $remediationApplied
            human_scope = $humanScope
            design_context = if ($designContextEmpty) { 'empty' } else { 'resolved' }
            phase_durations_seconds = [pscustomobject]$phaseTimings
        }
        if ($Extra) { foreach ($k in $Extra.Keys) { $obj[$k] = $Extra[$k] } }
        [System.IO.File]::WriteAllText($statusPath, (([pscustomobject]$obj) | ConvertTo-Json -Depth 8))
    }
    try {
        & $writeStatus 'running' @{ phase = 'initializing' }
        $currentPhase = 'baseline-resolution'
        & $recordPhaseStart $currentPhase
        if ([string]::IsNullOrWhiteSpace($BaselineRef)) { $BaselineRef = Resolve-ContinuousCoReviewWorktreeBaseline -RepoRoot $RepoRoot }
        & $recordPhaseEnd 'baseline-resolution'
        if ([string]::IsNullOrWhiteSpace($BaselineRef)) { & $writeStatus 'failed' @{ failure_reason = 'baseline-unresolved' }; return (Get-Content $statusPath -Raw | ConvertFrom-Json) }
        $currentPhase = 'design-context-resolution'
        & $recordPhaseStart $currentPhase
        # T034b: legacy worktree review and the production campaign path share this exact
        # fail-before-selection decision and component-wise physical-containment primitive.
        $designContextSelection = Resolve-ContinuousCoReviewDesignContextSelection -RepoRoot $RepoRoot -DesignContextFiles $DesignContextFiles
        if (-not $designContextSelection.valid) {
            & $writeStatus 'failed' @{ failure_reason = $designContextSelection.reason; unresolved_design_context = @($designContextSelection.unresolved_refs) }
            return (Get-Content $statusPath -Raw | ConvertFrom-Json)
        }
        $DesignContextFiles = @($designContextSelection.resolved_refs)
        # f1 (codex 2026-07-08): an EMPTY design context is RECORDED + DEGRADES the run - never a
        # silent blind review. The reviewer is told (prompt note), status.json carries it, and the
        # done-write forces completeness=partial so the T094 tier demands a human ack for the
        # evidence. Not a terminal failure: a genuinely spec-less repo (greenfield empty-tree
        # baseline) may legitimately review code-only.
        $designContextEmpty = [bool]$designContextSelection.design_context_empty
        if ($designContextEmpty) {
            [Console]::Error.WriteLine('[co-review] WARN DESIGN_CONTEXT_EMPTY no spec/design-analysis resolved (.review/design will be empty); the run is labelled partial and the reviewer is told to flag it.')
        }
        & $recordPhaseEnd 'design-context-resolution'

        # SELECT the reviewer host: independent-preferred + authorized, labelled (T093/FR-035). A
        # same-host fallback FIRES immediately (never blocks); the label makes it first-class evidence.
        # Fail-soft (stated reason) if none - and an un-honourable explicit --host is SURFACED, never
        # silently substituted.
        $currentPhase = 'reviewer-host-selection'
        & $recordPhaseStart $currentPhase
        $reviewerHost = Resolve-ContinuousCoReviewReviewerHost -RepoRoot $RepoRoot -CodeWriterHost $CodeWriterHost -RequestedHost $RequestedHost
        & $recordPhaseEnd 'reviewer-host-selection'
        if ($null -eq $reviewerHost) {
            $selFailure = if (-not [string]::IsNullOrWhiteSpace($RequestedHost)) {
                "requested-host-not-available: '$RequestedHost' is not installed+authorized (an explicit --host is honoured or surfaced, never silently substituted)"
            }
            else { 'no-authorized-reviewer-host' }
            & $writeStatus 'failed' @{ failure_reason = $selFailure; requested_reviewer_host = $RequestedHost }
            return (Get-Content $statusPath -Raw | ConvertFrom-Json)
        }
        # Defensive normalization (regression guard): the status writes below read
        # $reviewerHost.independence_source (added by the T012 live-door provenance). A host object
        # that omits it - a legacy/stubbed selector - must NOT crash the orchestrator under
        # StrictMode; default the provenance to 'unverified' (the SEC-004 fail-closed sense).
        if (-not ($reviewerHost.PSObject.Properties.Name -contains 'independence_source')) {
            $reviewerHost | Add-Member -NotePropertyName 'independence_source' -NotePropertyValue 'unverified' -Force
        }

        # The reviewed-state DIGEST = the gate's identity. Get-...SignoffGateDecision compares ITS current digest to a
        # passing run's recorded reviewed_tree_id. Computed BEFORE materialization (identity-unification fix,
        # escalation 20260708T211331029): the worktree is materialized FROM this digest tree, so the reviewed
        # content and the certified content are the SAME git tree object by construction - uncommitted changes
        # are REVIEWED, never silently certified (the FR-025 false-allow the reviewer escalated). Computed over
        # the MAIN repo (the worktree is a bare git-archive extract, NOT a git repo). _load is dot-sourced only
        # inside Resolve-...ReviewerHost's scope, so lazy-load it for THIS scope.
        if (-not (Get-Command -Name 'Get-ContinuousCoReviewReviewedStateDigest' -ErrorAction SilentlyContinue)) {
            $lp = Join-Path $PSScriptRoot '_load.ps1'; if (Test-Path -LiteralPath $lp -PathType Leaf) { try { . $lp } catch { $null = $_ } }
        }
        # SURFACE a digest failure (do not swallow it to ''): an empty digest makes the gate's freshness loop skip the
        # record -> a genuinely clean review blocks as 'stale' with no visible cause. Carry the reason in the status.
        # On digest failure the materialization falls back to HEAD (reviewedDigestErr says why the identities differ).
        $currentPhase = 'reviewed-state-digest'
        & $recordPhaseStart $currentPhase
        $reviewedDigestId = ''; $reviewedDigestErr = ''
        try { $dg = Get-ContinuousCoReviewReviewedStateDigest -RepoRoot $RepoRoot; if ($null -ne $dg -and $dg.ok) { $reviewedDigestId = [string]$dg.tree_id } else { $reviewedDigestErr = if ($null -ne $dg) { [string]$dg.failure_reason } else { 'digest-unavailable' } } } catch { $reviewedDigestErr = $_.Exception.Message }
        & $recordPhaseEnd 'reviewed-state-digest'

        $currentPhase = 'worktree-materialization'
        & $recordPhaseStart $currentPhase
        & $writeStatus 'running' @{ baseline_ref = $BaselineRef; reviewer_host = $reviewerHost.host; reviewer_independence = $reviewerHost.independence; independence_source = $reviewerHost.independence_source; reviewer_selection_reason = $reviewerHost.selection_reason; requested_reviewer_host = $RequestedHost }
        $wt = New-ContinuousCoReviewStrippedWorktree -RepoRoot $RepoRoot -BaselineRef $BaselineRef -DesignContextFiles $DesignContextFiles -SourceTreeId $reviewedDigestId
        & $recordPhaseEnd 'worktree-materialization'
        # T092 pre-flight generous-budget bump REVERTED (Issue 1): a 30-min AUTO checkpoint review is wrong - the
        # navigator fires one on every checkpoint, and even fully detached it should be SHORT. The auto path now
        # uses the passed-in budget (the navigator default). The generous budget belongs to manual
        # `specrew review --live` (where the human explicitly waits) and, for the auto path, is superseded by the
        # Phase-2 activity watchdog (kill on inactivity, not a fixed wall). The helpers
        # (Get-ContinuousCoReviewGenerousBudget / Test-ContinuousCoReviewExplicitTimeoutConfigured) + $wt.diff_bytes
        # are retained (unit-tested) for manual-path / Phase-2 reuse.
        # T111 (DEC-197-I010-004): inject the implementer's MACHINE-RECORDED test evidence into the worktree -
        # ONLY on an exact digest match with the tree under review, so the reviewer can substitute the recorded
        # suites for broad re-runs (the budget-death fix). A mismatch or absence injects NOTHING (never wrong
        # evidence); the prompt block is gated on the same flag so the reviewer is never told to trust a file
        # that is not there.
        $implementerEvidencePresent = $false
        try {
            if (Get-Command -Name 'Copy-ContinuousCoReviewImplementerEvidence' -ErrorAction SilentlyContinue) {
                # THE ACTUAL SELECTED PLAN into the injection boundary (maintainer wiring directive 2026-07-15):
                # resolved via the one production seam (the FR-049 supplier's canonical output), NEVER derived
                # from the evidence. Unavailable/invalid -> $null, and the copy then withholds plan-identified
                # runs fail-closed as selected-plan-unavailable.
                $selectedPlan = $null
                try {
                    if (Get-Command -Name 'Get-ContinuousCoReviewSelectedVerificationPlan' -ErrorAction SilentlyContinue) {
                        $selRes = Get-ContinuousCoReviewSelectedVerificationPlan -RepoRoot $RepoRoot
                        if ($null -ne $selRes -and [bool]$selRes.available) { $selectedPlan = $selRes.plan }
                    }
                }
                catch { $selectedPlan = $null }
                $implementerEvidencePresent = [bool](Copy-ContinuousCoReviewImplementerEvidence -RepoRoot $RepoRoot -WorktreePath $wt.worktree_path -DigestTreeId $reviewedDigestId -Plan $selectedPlan)
            }
        } catch { $implementerEvidencePresent = $false }
        # ROUND: same lineage (change-set overlaps the prior round's) + the prior was blocking -> this is a fix
        # re-review (round+1, thread the prior findings); else a new checkpoint (round 1, no prior). The reviewer
        # escalates at the final round (the counter is the safety ceiling).
        $currentPhase = 'round-state-resolution'
        & $recordPhaseStart $currentPhase
        $maxRounds = Get-ContinuousCoReviewMaxRounds -RepoRoot $RepoRoot
        $prior = Get-ContinuousCoReviewRoundState -RepoRoot $RepoRoot
        $round = 1; $priorFindings = $null
        if ($null -ne $prior -and ([bool]$prior.blocking) -and (Test-ContinuousCoReviewPathLineageOverlap -Current @($wt.changed_paths) -Prior @($prior.changed_paths))) {
            $round = [int]$prior.round + 1
            $priorFindings = [string]$prior.findings
        }
        & $recordPhaseEnd 'round-state-resolution'
        try {
            # T096: a HUMAN-DIRECTED rerun (a consumed remediation) is never auto-halted - the round
            # ceiling guards the AUTO loop; the menu is its escape hatch.
            if (($round -gt $maxRounds) -and ($null -eq $remediationApplied)) {
                # CEILING REACHED — do NOT fire another review round (the deterministic spin-stop / round-9 fix: a
                # round>maxRounds guard provably halts the monotonic climb while the change-set overlaps). BUT a halt
                # is NOT a clean pass. The old code wrote an EMPTY result here, so the run read as
                # 'done / 0 findings / clean' and SILENTLY passed an UNREVIEWED increment — the false-green
                # (D-197-I009-010) that fooled a dogfood coordinator into signing off code the reviewer never saw.
                # Instead emit a VISIBLE escalation finding so the run can NEVER be read as clean: kind='escalation'
                # -> Option A parks it as escalated_to_human (the gate does NOT deadlock); severity 'blocking' -> the
                # navigator surfaces a NOT-REVIEWED stop-block. Persist a STICKY round-state (blocking=true) so
                # overlapping checkpoints stay above the ceiling and review stays bounded for THIS path-set (resets
                # when the change-set no longer overlaps). status carries reviewed=false so any consumer reading
                # status.json knows the increment was NOT reviewed. (F-197 iter-009 Option A #2 + D-010 hardening.)
                $currentPhase = 'ceiling-halt'
                & $recordPhaseStart $currentPhase
                $resolvedCount = 0
                if ($null -ne $prior -and ($prior.PSObject.Properties.Name -contains 'dispositions') -and $null -ne $prior.dispositions) {
                    $resolvedCount = @($prior.dispositions | Where-Object { [string]$_.state -eq 'resolved-against-disk' }).Count
                }
                # HONEST SPEND COUNT (finding 9e3a44f1): $round here is the PROSPECTIVE round (prior.round+1)
                # whose invocation the ceiling PREVENTS - it never reviewed. FR-019/T020 count only rounds that
                # ACTUALLY reviewed, so the halt message and the persisted state report $maxRounds (the rounds
                # that ran and hit the limit), never the +1 attempt. Persisting $maxRounds (not $round) keeps the
                # sticky latch working - the next overlapping checkpoint still computes maxRounds+1 > maxRounds.
                $spentRounds = [Math]::Min([int]$round, [int]$maxRounds)
                [System.IO.File]::WriteAllText($resultOut, (New-ContinuousCoReviewCeilingEscalationResult -RunId $RunId -Round $spentRounds -MaxRounds $maxRounds -ResolvedAgainstDiskCount $resolvedCount))
                Set-ContinuousCoReviewRoundState -RepoRoot $RepoRoot -ChangedPaths @($wt.changed_paths) -Round $spentRounds -Blocking $true -Findings $priorFindings
                & $recordPhaseEnd 'ceiling-halt'
                $runTimer.Stop()
                & $writeStatus 'done' @{ baseline_ref = $BaselineRef; changed_count = $wt.changed_count; changed_paths = @($wt.changed_paths); tree_id = $wt.tree_id; reviewed_digest_tree_id = $reviewedDigestId; reviewed_digest_error = $reviewedDigestErr; reviewer_host = $reviewerHost.host; reviewer_independence = $reviewerHost.independence; independence_source = $reviewerHost.independence_source; round = $spentRounds; max_rounds = $maxRounds; blocking = $false; ceiling_halted = $true; reviewed = $false }
                return (Get-Content $statusPath -Raw | ConvertFrom-Json)
            }
            # T020 PREFLIGHT (FR-019 two-budget accounting): the review INPUT must be materialized
            # BEFORE the model is invoked. A missing .review/changes.diff is an INFRASTRUCTURE failure
            # (the field da2bc5cc class where the engine did not materialize its own input) - it must
            # consume NEITHER provider budget NOR a round-allowance slot, so we fail HERE without
            # invoking the reviewer and without touching the round-state. Distinct from an invoked
            # failure below (which DID spend and DOES count).
            $changesDiffPath = Join-Path $wt.worktree_path '.review/changes.diff'
            if (-not (Test-Path -LiteralPath $changesDiffPath -PathType Leaf)) {
                $preflightSpend = Get-ContinuousCoReviewRoundSpendClass -InputMaterialized $false -ModelInvoked $false -ProducedReview $false
                $runTimer.Stop()
                & $writeStatus 'failed' @{ failure_reason = 'input-not-materialized'; spend_class = $preflightSpend.class; provider_spend = $preflightSpend.records_provider_spend; round_consumed = $preflightSpend.consumes_round; reviewer_host = $reviewerHost.host; reviewer_independence = $reviewerHost.independence; independence_source = $reviewerHost.independence_source; reviewed = $false }
                return (Get-Content $statusPath -Raw | ConvertFrom-Json)
            }
            # T015 (FR-010) SIMPLIFIED (maintainer option-1 decision, 2026-07-11): the orchestrator does
            # NOT auto-run declared verification, copy a sandbox, or re-run commands per review. Automatic
            # reruns were removed because they could not be confined in-process - the copy was not an OS
            # boundary (ambient-authority escape, finding 4b124d0e), Copy-Item raced concurrent host churn
            # (c9abe16d), unbounded output had to be capped (bfc7b5c5), and the mechanism fought T111's
            # anti-budget-death design. Those cases survive as regression evidence in
            # bounded-verification.Tests.ps1, documenting WHY. Runner-observed verification is now T018's
            # job: verification runs ONCE through the recorded-run wrapper and the digest-bound evidence is
            # injected as .review/implementer-evidence.json (above) for the reviewer to READ + spot-check.
            # Invoke-ContinuousCoReviewBoundedVerification remains an EXPLICIT opt-in API only.
            #
            # REVIEWER-INVOCATION INTEGRITY: the reviewer is itself an agentic host with ambient authority.
            # Hash the source + authoritative reviewer inputs (.review/changes.diff, design/, process/,
            # implementer-evidence) immediately BEFORE and AFTER the reviewer runs; the ONLY permitted write
            # is the reviewer's own output (.review/findings.jsonl). Any other mutation FAILS the review -
            # the reviewer must inspect, never edit, the certified fire-time tree. This is MONITORED
            # confinement, not OS-enforced filesystem isolation (a dedicated process identity + worktree-only
            # ACL isolation is recorded as a separate future proposal, not T015 scope).
            $reviewerOutputAllow = @('.review/findings.jsonl')
            $reviewerPreHashes = Get-ContinuousCoReviewWorktreeSourceHashes -WorktreePath $wt.worktree_path
            # T016 (FR-011/SC-003) MONITORED containment: the origin roots the reviewer tree must NOT reach - the
            # SAME two T013 guards against (the governance RepoRoot + the git top-level). Violations accumulate
            # PASSIVELY across heartbeats (NEVER a mid-flight kill); the loud origin-side fail is applied at the
            # reviewer's natural end (below). child_pid rides the existing reviewer telemetry.
            $containmentOriginRoots = @($RepoRoot)
            try { $cgt = (& git -C $RepoRoot rev-parse --show-toplevel 2>$null); if (-not [string]::IsNullOrWhiteSpace($cgt)) { $containmentOriginRoots += ([string]$cgt).Trim() } } catch { $null = $_ }
            $containmentViolations = [System.Collections.Generic.List[object]]::new()
            $containmentSeen = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
            # FR-011 amended (maintainer review 2026-07-12): the monitor records its own HEALTH so weak visibility is
            # never silent inactivity. A hashtable (reference type) so the heartbeat closure's mutations persist.
            $samplerHealth = @{ attempts = 0; successful_samples = 0; failures = 0; last_reason = ''; final_sample_taken = $false; cadence_seconds = 5 }
            $currentPhase = 'reviewer-execution'
            & $writeStatus 'running' @{ baseline_ref = $BaselineRef; changed_count = $wt.changed_count; tree_id = $wt.tree_id; reviewed_digest_tree_id = $reviewedDigestId; reviewed_digest_error = $reviewedDigestErr; reviewer_host = $reviewerHost.host; reviewer_independence = $reviewerHost.independence; independence_source = $reviewerHost.independence_source; round = $round; max_rounds = $maxRounds; blocking = $null; implementer_evidence = $implementerEvidencePresent }
            & $recordPhaseStart $currentPhase
            $reviewerHeartbeat = {
                param($Telemetry)
                # T016: ride the heartbeat to SAMPLE the reviewer tree (read-only) and accumulate any origin-access
                # violation. Strictly monitoring - it never mutates or kills the reviewer; the loud fail is applied
                # after the reviewer's natural end. Assign the `, (...)`-returning helpers to vars (never @()/inline
                # foreach) so a single sample/violation is not double-wrapped.
                if ($null -ne $Telemetry -and $Telemetry.PSObject.Properties['child_pid'] -and $Telemetry.child_pid -and $containmentOriginRoots.Count -gt 0) {
                    # A running=false telemetry is the FINAL post-exit sample: its vanished tree is EXPECTED, so it is
                    # recorded as taken but its emptiness is NOT counted as a sampler failure.
                    $isFinal = ($Telemetry.PSObject.Properties['running'] -and (-not $Telemetry.running))
                    $samplerHealth.attempts++
                    try {
                        # DRIFT-198-I003-004: STRUCTURED argv (single-arg prompt is not a path; quoted/relative/option
                        # forms covered best-effort). -Health returns the monitor's own visibility so it is never silent.
                        $cvHealth = $null
                        $cvSamples = Get-ContinuousCoReviewContainmentSamples -RootPid ([int]$Telemetry.child_pid) -WorktreeCwd $wt.worktree_path -Health ([ref]$cvHealth)
                        $cvNew = Test-ContinuousCoReviewContainmentViolations -Samples $cvSamples -OriginRoots $containmentOriginRoots -RunId $RunId
                        foreach ($cv in $cvNew) {
                            $cvKey = ('{0}|{1}|{2}' -f $cv.process, $cv.source, $cv.path)
                            if ($containmentSeen.Add($cvKey)) { [void]$containmentViolations.Add($cv) }
                        }
                        if ($isFinal) { $samplerHealth.final_sample_taken = $true }
                        elseif ($null -ne $cvHealth -and $cvHealth.degraded) { $samplerHealth.failures++; $samplerHealth.last_reason = [string]$cvHealth.reason }
                        else { $samplerHealth.successful_samples++ }
                    }
                    catch { if (-not $isFinal) { $samplerHealth.failures++; $samplerHealth.last_reason = 'sampler-exception' } }
                }
                & $writeStatus 'running' @{ baseline_ref = $BaselineRef; changed_count = $wt.changed_count; tree_id = $wt.tree_id; reviewed_digest_tree_id = $reviewedDigestId; reviewed_digest_error = $reviewedDigestErr; reviewer_host = $reviewerHost.host; reviewer_independence = $reviewerHost.independence; independence_source = $reviewerHost.independence_source; round = $round; max_rounds = $maxRounds; blocking = $null; reviewer_telemetry = $Telemetry }
            }
            $r = Invoke-ContinuousCoReviewWorktreeReviewer -WorktreePath $wt.worktree_path -RunId $RunId -HostName $reviewerHost.host -RoundNumber $round -MaxRounds $maxRounds -PriorFindings $priorFindings -TimeoutSeconds $TimeoutSeconds -Heartbeat $reviewerHeartbeat -HumanScope $humanScope -DesignContextEmpty:$designContextEmpty -ImplementerEvidencePresent:$implementerEvidencePresent
            & $recordPhaseEnd 'reviewer-execution'
            # INTEGRITY CHECK: did the reviewer mutate the certified tree beyond its allowed output? The
            # model WAS invoked (provider spend real), so a violation is the invoked-failed class (spend +
            # round consumed, distinct disposition) and the findings are DISCARDED - a reviewer that edited
            # the tree it certifies cannot be trusted.
            $reviewerPostHashes = Get-ContinuousCoReviewWorktreeSourceHashes -WorktreePath $wt.worktree_path
            $reviewerTamperedPaths = New-Object System.Collections.Generic.List[string]
            # MODIFIED or DELETED pre-existing file (source, .review authority, OR tracked config under a
            # host dir) -> tampering, unless it is the reviewer's own findings output.
            foreach ($k in $reviewerPreHashes.Keys) { if ((-not $reviewerPostHashes.ContainsKey($k) -or $reviewerPostHashes[$k] -ne $reviewerPreHashes[$k]) -and ($reviewerOutputAllow -notcontains $k)) { [void]$reviewerTamperedPaths.Add($k) } }
            # NEW file -> tampering, UNLESS it is the reviewer's findings output OR legitimate host session
            # state (a NEW file under a volatile host-runtime dir). A new file anywhere else is planted.
            foreach ($k in $reviewerPostHashes.Keys) { if ((-not $reviewerPreHashes.ContainsKey($k)) -and ($reviewerOutputAllow -notcontains $k) -and -not (Test-ContinuousCoReviewIsHostChurnPath -RelativePath $k)) { [void]$reviewerTamperedPaths.Add($k) } }
            if ($reviewerTamperedPaths.Count -gt 0) {
                $currentPhase = 'reviewer-tamper'
                $tamperSpend = Get-ContinuousCoReviewRoundSpendClass -InputMaterialized $true -ModelInvoked $true -ProducedReview $false
                [System.IO.File]::WriteAllText($resultOut, '')
                $priorBlockingT = ($null -ne $prior) -and [bool]$prior.blocking
                $priorFindingsT = if ($null -ne $prior) { [string]$prior.findings } else { $null }
                Set-ContinuousCoReviewRoundState -RepoRoot $RepoRoot -ChangedPaths @($wt.changed_paths) -Round $round -Blocking $priorBlockingT -Findings $priorFindingsT
                $null = Add-ContinuousCoReviewRoundDisposition -RepoRoot $RepoRoot -Disposition ([pscustomobject][ordered]@{ state = 'reviewer-tampered-tree'; run_id = $RunId; round = $round; provider_spend = $true; tampered_paths = @($reviewerTamperedPaths | Select-Object -First 10); recorded_at = (ConvertTo-ContinuousCoReviewWorktreeIsoTimestamp) })
                $runTimer.Stop()
                & $writeStatus 'failed' @{ failure_reason = 'reviewer-tampered-tree'; message = ('the reviewer altered the certified tree beyond its allowed output (.review/findings.jsonl): ' + (@($reviewerTamperedPaths | Select-Object -First 10) -join ', ')); spend_class = $tamperSpend.class; provider_spend = $tamperSpend.records_provider_spend; round_consumed = $tamperSpend.consumes_round; reviewer_host = $reviewerHost.host; reviewer_independence = $reviewerHost.independence; independence_source = $reviewerHost.independence_source; reviewed = $false }
                return (Get-Content $statusPath -Raw | ConvertFrom-Json)
            }
            # T016 (FR-011/SC-003): CONTAINMENT VIOLATION. If the reviewer tree was observed reaching origin during
            # the run (accumulated PASSIVELY across heartbeats - never a mid-flight kill), fail the run LOUDLY with
            # an origin-side record. The model WAS invoked (provider spend real) -> invoked-failed class + a distinct
            # disposition; the findings are DISCARDED (a reviewer that reached origin cannot be trusted). Records
            # carry ONLY bounded/redacted path/process metadata (never the raw cmdline/prompt/env/creds), and only
            # in these ORIGIN-SIDE writes (status.json + disposition), never the reviewer worktree.
            # FR-011 AMENDED (maintainer review 2026-07-12): partition observed origin access by SIGNAL STRENGTH. A
            # STRONG signal (a reviewer-tree process whose cwd or exe resolves under origin) hard-fails LOUDLY and
            # DISCARDS findings. A command-line ARGUMENT under origin is a best-effort DIAGNOSTIC WARNING (argv matching
            # is inherently incomplete) - recorded, but it NEVER by itself discards an otherwise valid review. The
            # STRUCTURAL guarantee is FR-008/T013.
            $strongViolations = @($containmentViolations | Where-Object { $_.source -in @('cwd', 'exe') })
            $containmentArgWarnings = @($containmentViolations | Where-Object { $_.source -eq 'arg' } | Select-Object -First 20)
            if ($strongViolations.Count -gt 0) {
                $currentPhase = 'containment-violated'
                $cvSpend = Get-ContinuousCoReviewRoundSpendClass -InputMaterialized $true -ModelInvoked $true -ProducedReview $false
                [System.IO.File]::WriteAllText($resultOut, '')
                $priorBlockingCv = ($null -ne $prior) -and [bool]$prior.blocking
                $priorFindingsCv = if ($null -ne $prior) { [string]$prior.findings } else { $null }
                Set-ContinuousCoReviewRoundState -RepoRoot $RepoRoot -ChangedPaths @($wt.changed_paths) -Round $round -Blocking $priorBlockingCv -Findings $priorFindingsCv
                $cvRecords = @($strongViolations | Select-Object -First 10)
                $null = Add-ContinuousCoReviewRoundDisposition -RepoRoot $RepoRoot -Disposition ([pscustomobject][ordered]@{ state = 'containment-violated'; run_id = $RunId; round = $round; provider_spend = $true; violations = $cvRecords; arg_warnings = $containmentArgWarnings; sampler_health = $samplerHealth; recorded_at = (ConvertTo-ContinuousCoReviewWorktreeIsoTimestamp) })
                $runTimer.Stop()
                & $writeStatus 'failed' @{ failure_reason = 'containment-violated'; message = ('the reviewer process tree accessed origin via a STRONG signal (cwd/exe under origin): ' + (@($cvRecords | ForEach-Object { $_.path } | Select-Object -First 5) -join ', ')); containment_violations = $cvRecords; containment_warnings = $containmentArgWarnings; sampler_health = $samplerHealth; spend_class = $cvSpend.class; provider_spend = $cvSpend.records_provider_spend; round_consumed = $cvSpend.consumes_round; reviewer_host = $reviewerHost.host; reviewer_independence = $reviewerHost.independence; independence_source = $reviewerHost.independence_source; reviewed = $false }
                return (Get-Content $statusPath -Raw | ConvertFrom-Json)
            }
            $reviewerTelemetry = if ($r.PSObject.Properties['telemetry']) { $r.telemetry } else { $null }
            $raw = [string]$r.stdout
            $resultSource = 'stdout'
            # FILE-PRIMARY (2026-07-12): the wrapper already FULLY contract-validated a clean-exit, current-run,
            # schema-valid .review/findings.jsonl delivered by a host that writes the file and exits with EMPTY
            # stdout (codex). Integrity has ALREADY passed (the tamper check above returned on any violation), so
            # this is a COMPLETE review - completeness='full', NOT the empty-stdout -> lenient-harvest -> 'partial'
            # path. A genuinely empty result carries no file_primary_result and falls through to stdout + harvest.
            if (($r.PSObject.Properties['file_primary_result']) -and -not [string]::IsNullOrWhiteSpace([string]$r.file_primary_result)) {
                $json = [string]$r.file_primary_result
                $completeness = 'full'
                $resultSource = 'file-primary'
            }
            else {
                $json = Get-ContinuousCoReviewFindingsJson -Raw $raw   # robust: fence -> span -> balanced-scan, validated
                $completeness = 'full'
                if ([string]::IsNullOrWhiteSpace($json)) {
                    # T090/R1: the final blob is empty/unparseable (a timeout / cut-short run). HARVEST the incremental
                    # .review/findings.jsonl prefix (or prose-salvage) so a degraded review still surfaces findings
                    # (any review > nothing), instead of discarding the whole run as 'no-parseable-findings-json'.
                    $json = Get-ContinuousCoReviewHarvestedPartialResult -WorktreePath $wt.worktree_path -RawStdout $raw -RunId $RunId
                    if (-not [string]::IsNullOrWhiteSpace($json)) { $completeness = 'partial' }
                }
            }
            # T096: a human-SCOPED review covered a SUBSET of the increment - its evidence is honestly
            # PARTIAL (the T094 tiered gate then requires the recorded ack, never a silent full pass).
            if (-not [string]::IsNullOrWhiteSpace([string]$humanScope)) { $completeness = 'partial' }
            # f1: a review that ran WITHOUT design context could not validate design conformance -
            # same honest degradation (partial -> the T094 ack tier), never silent full evidence.
            if ($designContextEmpty) { $completeness = 'partial' }
            if (-not [string]::IsNullOrWhiteSpace($json)) {
                $currentPhase = 'write-result'
                & $recordPhaseStart $currentPhase
                [System.IO.File]::WriteAllText($resultOut, $json)
                # detect a blocking finding -> feed the next round's lineage decision
                $blocking = $false
                try { foreach ($f in @(($json | ConvertFrom-Json -Depth 100).findings)) { if (([string]$f.severity) -match '(?i)block') { $blocking = $true; break } } } catch { $null = $_ }
                Set-ContinuousCoReviewRoundState -RepoRoot $RepoRoot -ChangedPaths @($wt.changed_paths) -Round $round -Blocking $blocking -Findings $json
                & $recordPhaseEnd 'write-result'
                $currentPhase = 'complete'
                $runTimer.Stop()
                & $writeStatus 'done' @{ baseline_ref = $BaselineRef; changed_count = $wt.changed_count; changed_paths = @($wt.changed_paths); tree_id = $wt.tree_id; reviewed_digest_tree_id = $reviewedDigestId; reviewed_digest_error = $reviewedDigestErr; reviewer_host = $reviewerHost.host; reviewer_independence = $reviewerHost.independence; independence_source = $reviewerHost.independence_source; round = $round; max_rounds = $maxRounds; blocking = $blocking; completeness = $completeness; result_source = $resultSource; implementer_evidence = $implementerEvidencePresent; reviewer_telemetry = $reviewerTelemetry; containment_warnings = $containmentArgWarnings; sampler_health = $samplerHealth }
            }
            else {
                $currentPhase = 'write-failure'
                & $recordPhaseStart $currentPhase
                [System.IO.File]::WriteAllText($resultOut, '')
                # STATE the reason: capture exit code + a stderr tail so an unparseable/empty verdict is diagnosable
                # (the agent invocation otherwise drops stderr and the failure is invisible).
                $stderrTail = if (-not [string]::IsNullOrWhiteSpace([string]$r.stderr)) { (([string]$r.stderr) -split "`n" | Where-Object { $_ } | Select-Object -Last 3) -join ' | ' } else { '' }
                # T020 (FR-019): the model WAS invoked (provider spend incurred) but produced no valid
                # review. Two-budget accounting: record provider spend AND consume a round-allowance slot
                # with a distinct failed-invocation disposition - it never disappears from accounting.
                # The prior blocking/findings are PRESERVED (a failed invocation reviewed nothing, so it
                # resolves nothing); the round is recorded as consumed.
                $failedSpend = Get-ContinuousCoReviewRoundSpendClass -InputMaterialized $true -ModelInvoked $true -ProducedReview $false
                $priorBlocking = ($null -ne $prior) -and [bool]$prior.blocking
                $priorFindingsStr = if ($null -ne $prior) { [string]$prior.findings } else { $null }
                Set-ContinuousCoReviewRoundState -RepoRoot $RepoRoot -ChangedPaths @($wt.changed_paths) -Round $round -Blocking $priorBlocking -Findings $priorFindingsStr
                $null = Add-ContinuousCoReviewRoundDisposition -RepoRoot $RepoRoot -Disposition ([pscustomobject][ordered]@{ state = 'failed-invocation'; run_id = $RunId; round = $round; provider_spend = $true; recorded_at = (ConvertTo-ContinuousCoReviewWorktreeIsoTimestamp) })
                & $recordPhaseEnd 'write-failure'
                $currentPhase = 'failed'
                $runTimer.Stop()
                & $writeStatus 'failed' @{ failure_reason = 'no-parseable-findings-json'; spend_class = $failedSpend.class; provider_spend = $failedSpend.records_provider_spend; round_consumed = $failedSpend.consumes_round; exit_code = $r.exit_code; stderr_tail = $stderrTail; reviewer_independence = $reviewerHost.independence; independence_source = $reviewerHost.independence_source; reviewer_telemetry = $reviewerTelemetry }
            }
        }
        finally {
            $currentPhase = 'cleanup'
            & $recordPhaseStart $currentPhase
            Remove-Item -LiteralPath $wt.worktree_path -Recurse -Force -ErrorAction SilentlyContinue
            & $recordPhaseEnd 'cleanup'
        }
    }
    catch {
        $currentPhase = 'failed'
        $runTimer.Stop()
        & $writeStatus 'failed' @{ failure_reason = 'orchestrator-exception'; message = ([string]$_.Exception.Message) }
    }
    if (Test-Path -LiteralPath $statusPath) { return (Get-Content $statusPath -Raw | ConvertFrom-Json) }
}