extensions/specrew-speckit/scripts/readiness-verdict.ps1

[CmdletBinding()]
param(
    [string]$ProjectPath = '.',
    # The boundary the readiness check is for. before-implement is the one that reached a consumer with its
    # verdict summarized away (B4F-071); the rule is the same for any human-judgment boundary.
    [string]$Boundary = 'before-implement',
    [switch]$AsJson
)

# THE OVERALL VERDICT LINE, DERIVED FROM THE LEDGER, NOT FROM THE AGENT'S REPORT (B4F-071, 2026-09-10).
#
# On the router-skill project the before-implement readiness sub-agent read the ledger correctly and wrote
# "Overall verdict: BLOCKED for implementation - no tasks -> before-implement authorization exists". The crew
# summarized the passing artifact checks as PASS, dropped that line, and announced implementation. The only
# thing between BLOCKED and the first product-source task was the maintainer reading the packet.
#
# This script prints that one line from the ledger. The governed before-implement command runs it and its
# output is the FIRST line of the readiness report, verbatim; the coordinator quotes it verbatim in the
# packet. A summary can drop a sentence an agent wrote; it has no reason to drop a line a script printed
# whose absence the packet rule names. Exit 0 either way: the line is the evidence, not a gate - the gate
# that cannot be summarized away is the Stop hook's source-write guard (beta5 first item).

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

$sharedGovernancePath = Join-Path $PSScriptRoot 'shared-governance.ps1'
if (-not (Test-Path -LiteralPath $sharedGovernancePath -PathType Leaf)) { throw "Missing shared governance helper '$sharedGovernancePath'." }
. $sharedGovernancePath

$root = Resolve-ProjectPath -Path $ProjectPath
$canonical = Normalize-SpecrewCanonicalBoundaryType -Boundary $Boundary
if ([string]::IsNullOrWhiteSpace($canonical)) { throw "Unrecognized boundary '$Boundary'." }
$order = @(Get-SpecrewBoundaryOrder)
$targetIdx = [Array]::IndexOf($order, $canonical)

$lastAuthorized = ''
$pendingText = 'no crossing is pending'
$ledgerReadable = $true
$cycleLast = ''          # the furthest boundary authorized IN THIS ITERATION'S CYCLE
$previousCycleClosed = $false
try {
    $state = Get-SpecrewBoundaryEnforcementState -ProjectRoot $root
    if ($null -eq $state -or $null -eq $state.EffectiveState) { $ledgerReadable = $false }
    else {
        $lastAuthorized = [string]$state.EffectiveState['last_authorized_boundary']
        if ($state.State.Contains('pending_crossing') -and $null -ne $state.State['pending_crossing']) {
            $scope = ConvertTo-SpecrewBoundaryMap -Value $state.State['pending_crossing']
            if ($null -ne $scope) { $pendingText = ("the pending crossing is '{0} -> {1}'" -f [string]$scope['from_boundary'], [string]$scope['to_boundary']) }
        }
        # THE CYCLE, NOT THE ORDINAL (R2, the independent review of ebb7597f - fix 6's class in a script written
        # after fix 6). The ledger's verdicts are one sequence for the feature; iteration-closeout ends a cycle
        # and the next verdict opens the next iteration's. Readiness for a per-iteration boundary is answered
        # from the verdicts AFTER the last closeout - the current iteration's own - never from the previous
        # iteration's closeout, which sorts after before-implement in the lifecycle list and read as
        # implementation approval for an iteration that had none.
        # EFFECTIVE, NOT RAW (the follow-up review of d8be7878): raw verdict_history keeps every approval a
        # scoped correction invalidated - on purpose, it is the immutable record. The projection every other
        # reader consumes drops them. Reading raw here recovered an invalidated approval as current authority.
        $cycle = [System.Collections.Generic.List[string]]::new()
        foreach ($entry in @($state.EffectiveState['verdict_history'])) {
            $map = ConvertTo-SpecrewBoundaryMap -Value $entry
            if ($null -eq $map) { continue }
            $to = Normalize-SpecrewCanonicalBoundaryType -Boundary ([string]$map['to_boundary'])
            if ([string]::IsNullOrWhiteSpace($to)) { continue }
            if ($to -eq 'iteration-closeout') { $cycle.Clear(); $previousCycleClosed = $true; continue }
            $cycle.Add($to) | Out-Null
        }
        $cycleIdx = -1
        foreach ($to in $cycle) { $i = [Array]::IndexOf($order, $to); if ($i -gt $cycleIdx) { $cycleIdx = $i; $cycleLast = $to } }
        if ($cycle.Count -eq 0 -and -not $previousCycleClosed -and -not [string]::IsNullOrWhiteSpace($lastAuthorized) -and $lastAuthorized -ne 'iteration-closeout') {
            # A ledger with a cursor and no verdict rows (legacy shape): the cursor is the only evidence there is.
            $cycleLast = Normalize-SpecrewCanonicalBoundaryType -Boundary $lastAuthorized
        }
    }
}
catch { $ledgerReadable = $false }

$cycleIdx = if ([string]::IsNullOrWhiteSpace($cycleLast)) { -1 } else { [Array]::IndexOf($order, $cycleLast) }
$authorized = ($ledgerReadable -and $targetIdx -ge 0 -and $cycleIdx -ge $targetIdx)
$lastText = if ([string]::IsNullOrWhiteSpace($lastAuthorized)) { 'no boundary is authorized yet' }
elseif ($lastAuthorized -eq 'iteration-closeout' -and [string]::IsNullOrWhiteSpace($cycleLast)) { "the last authorization is the previous iteration's closeout ('iteration-closeout'); this iteration has no authorization yet" }
elseif ([string]::IsNullOrWhiteSpace($cycleLast)) { ("the ledger's last authorized boundary is '{0}' and this iteration's cycle has no authorization yet" -f $lastAuthorized) }
else { ("this iteration's cycle is authorized through '{0}'" -f $cycleLast) }
$line = if (-not $ledgerReadable) {
    ("Overall verdict: BLOCKED for implementation - the boundary ledger (.specrew/start-context.json) could not be read, so '{0}' cannot be shown as authorized." -f $canonical)
}
elseif ($authorized) {
    ("Overall verdict: READY for implementation - '{0}' is authorized ({1})." -f $canonical, $lastText)
}
else {
    ("Overall verdict: BLOCKED for implementation - '{0}' is not authorized: {1}; {2}. The human's typed 'approved for {0}' against the presented crossing is what clears this; nothing else does." -f $canonical, $lastText, $pendingText)
}

if ($AsJson) {
    [pscustomobject]@{ boundary = $canonical; authorized = $authorized; last_authorized_boundary = $lastAuthorized; pending = $pendingText; line = $line } | ConvertTo-Json -Compress
}
else {
    Write-Output $line
}
exit 0