diff --git a/scripts/coord/lane.ps1 b/scripts/coord/lane.ps1 index ae4e7848b7..c12a9cb0a1 100644 --- a/scripts/coord/lane.ps1 +++ b/scripts/coord/lane.ps1 @@ -2,11 +2,21 @@ # Copyright (C) 2026 MessageFoundry Foundation, LLC and contributors <# .SYNOPSIS - Record THIS lane's own free-slot count. The only writer of a lane level. + Record THIS lane's own counts of items in flight and items free by work. The only writer of a + lane level. .DESCRIPTION - A builder lane states how many of its slots are free BY WORK. Nothing else in the fleet may write - that number -- not the dispatcher, not a hook, not an inference from claims. + A builder lane states how many items it is working and how many it holds without working them. + Nothing else in the fleet may write those numbers -- not a Manager, not a hook, not an inference + from claims. + + THERE IS NO ITEM CEILING (owner ruling 2026-10-26). This header is the tooling's record of the + ruling; lane-level.ps1 and tests/test_coord_lane_no_ceiling.py cite it. The four-item lane + ceiling, the start-throttle by burn, any WIP limit on briefing, and the -Capacity parameter + that defaulted to 4 are retired. Builders and Managers work at full speed; usage is governed by + the Steward's holds and the korus Workflow gate, never by this script. Nothing it records is a + cap, a slot count or a reason to stop briefing or building. Do not add a capacity back, and do + not add a usage throttle here. THERE IS NO -Lane AND NO -BoxKey PARAMETER, AND ADDING ONE DEFEATS THE WHOLE DESIGN. The box key is derived from $PWD, so a session can only ever state its OWN level. The failure this replaces is a @@ -15,8 +25,11 @@ numbers were honest. Only the lane knows which is which. FREE MEANS BY WORK, NOT BY CLAIMS. Built-and-awaiting-land is FREE. Blocked is FREE. Parked on a - condition is FREE. A claim you are not currently able to progress is a FREE slot wearing an - occupied label, and that mislabelling is the outage. + condition is FREE. A claim you are not currently able to progress is FREE work wearing an + occupied label, and that mislabelling is the outage. With no ceiling there are no empty slots, + so -Free counts those held items and nothing else. A level stated before 2026-10-26 may also + count empty slots under the retired ceiling. lane-level.ps1 asks a lane in a worktree named + builder* for a fresh level after 15 minutes; a lane in a worktree named otherwise is never asked. #> [CmdletBinding(DefaultParameterSetName = 'State')] param( @@ -24,11 +37,11 @@ param( # set without both, and a Mandatory prompt there hangs a caller with stdin open # (tests/_no_prompt.py, vault BACKLOG #3627). The refusal follows this param block, after the # cloud refusal. - [Parameter(ParameterSetName = 'State')][ValidateRange(0, 99)][int]$Free, - [Parameter(ParameterSetName = 'State')][ValidateRange(0, 99)][int]$InFlight, + # Non-negative and unbounded above: an upper bound here would be a ceiling by another name. + [Parameter(ParameterSetName = 'State')][ValidateRange('NonNegative')][int]$Free, + [Parameter(ParameterSetName = 'State')][ValidateRange('NonNegative')][int]$InFlight, [Parameter(ParameterSetName = 'State')][string]$Note = '', - [Parameter(ParameterSetName = 'State')][ValidateRange(1, 99)][int]$Capacity = 4, - # Stamp that a dispatcher asked. Carries NO number and cannot produce an at-capacity reading. + # Stamp that a dispatcher asked. Carries NO number. [Parameter(ParameterSetName = 'Asked', Mandatory)][switch]$Asked, [Parameter(ParameterSetName = 'Show', Mandatory)][switch]$Show ) @@ -140,7 +153,7 @@ if (-not $Show -and $rec -and -not (Test-CoordRecordIsHere $rec)) { } if (-not $rec) { $rec = [pscustomobject]@{ - v = 1; boxKey = $boxKey; worktree = $worktree; capacity = $Capacity + v = 1; boxKey = $boxKey; worktree = $worktree; capacity = $null statedFree = $null; statedInFlight = $null; statedUtc = $null statedBySessionId = $null; statedNote = '' tipAtStated = ''; claimHashAtStated = ''; turnsSinceStated = 0 @@ -175,7 +188,7 @@ if ($Show) { # between the two reads would print a stated level beside an `absent` reason. The fields # match the blank record above; fold the two together once that read keeps its reason. $shown = [pscustomobject]@{ - v = 1; boxKey = $boxKey; worktree = $worktree; capacity = $Capacity + v = 1; boxKey = $boxKey; worktree = $worktree; capacity = $null statedFree = $null; statedInFlight = $null; statedUtc = $null statedBySessionId = $null; statedNote = '' tipAtStated = ''; claimHashAtStated = ''; turnsSinceStated = 0 @@ -224,7 +237,6 @@ if ($Asked) { $tip = (& git rev-parse HEAD 2>$null) if ($LASTEXITCODE -ne 0) { $tip = '' } else { $tip = $tip.Trim() } - $rec.capacity = $Capacity $rec.statedFree = $Free $rec.statedInFlight = $InFlight $rec.statedUtc = $nowUtc @@ -238,6 +250,19 @@ if ($Asked) { } $rec.worktree = $worktree $rec.boxKey = $boxKey +# `capacity` IS WRITTEN AS NULL, NEVER DROPPED. An older lane.ps1, run from another worktree's branch, +# assigns $rec.capacity on every statement, and assigning a property a PSCustomObject lacks throws, +# so that lane could not restate. Null is a value that names no cap; a record written before the +# ceiling was retired holds 4 there until a statement replaces it. This branch's lane-level.ps1 +# reads only whether it is null, as a marker of who stated the counts; its header says how. So only +# a statement nulls it: -Asked writes no counts, and nulling the marker there would leave an older +# statement's counts reading as this script's. An older copy of that hook falls back to 4 whatever +# this field holds, and reads 0 free with 2 or more in flight as full, which under the new meaning of +# -Free it does sooner. Only replacing that reader, wherever it is installed, retires the ceiling there. +if (-not $Asked) { $rec | Add-Member -NotePropertyName capacity -NotePropertyValue $null -Force } +elseif (-not ($rec.PSObject.Properties.Name -contains 'capacity')) { + $rec | Add-Member -NotePropertyName capacity -NotePropertyValue $null +} # docs/COORD-SHARED-ROOT.md: a reader on another PC must be able to tell this lane is not its own. $rec | Add-Member -NotePropertyName host -NotePropertyValue $thisHost -Force @@ -250,7 +275,7 @@ Move-CoordFileReplace -Source $tmp -Destination $file -ReplaceReadOnly if ($Asked) { Write-Host "lane: recorded that a dispatcher asked. No number written." } else { - Write-Host "lane: $boxKey -- $Free free / $InFlight in flight of $($rec.capacity), stated $nowUtc" + Write-Host "lane: $boxKey -- $Free free / $InFlight in flight, stated $nowUtc" if (-not $Note) { Write-Host " (no note. A note is what makes the number auditable later.)" } } exit 0 diff --git a/scripts/hooks/lane-level.ps1 b/scripts/hooks/lane-level.ps1 index 379581a436..d819c1d119 100644 --- a/scripts/hooks/lane-level.ps1 +++ b/scripts/hooks/lane-level.ps1 @@ -2,17 +2,30 @@ # Copyright (C) 2026 MessageFoundry Foundation, LLC and contributors <# .SYNOPSIS - Stop hook. In a builder session it nags that lane for its own free-slot count. In the dispatcher - session it reports those counts and speaks whenever it cannot prove both lanes are full. + Stop hook. In a builder session it nags that lane for its own level. In the dispatcher session + it reports each lane's stated counts, and it never calls a lane full. .DESCRIPTION - THE STRUCTURAL PROPERTY, AND EVERYTHING ELSE IS DETAIL: every input is either a lane's own - assertion of fullness, or an invalidator of one. NOTHING TURNS "UNKNOWN" INTO "FULL". A missing - file, an unreadable file, a stale number, a superseded number, a roster shortfall -- all of them - SPEAK. Silence is reachable only through a lane's own fresh statement. - - THAT IS WHY IT SURVIVES A DISPATCHER WITH NO DISCIPLINE: THE DISPATCHER WRITES NOTHING THIS HOOK - READS. It never reads mail, receipts, dispatch traffic, or claim counts to derive occupancy. + THERE IS NO ITEM CEILING. The owner ruling of 2026-10-26 is stated once, in the header of + scripts/coord/lane.ps1. This hook used to call a lane AT CAPACITY when it stated no free items, + against a four-item ceiling, and went quiet when both lanes said so. Now no count it reads + becomes a verdict that a lane is full or that briefing should stop, and it adds no usage + throttle of its own. The one count-driven word left is IDLE, for a lane with nothing in flight. + + AN OLDER lane.ps1 MEANS SOMETHING ELSE BY FREE. Its free count includes the empty slots under + the retired ceiling, so read as held items it overstates them. The current lane.ps1 writes + `capacity` as null on every statement and keeps whatever it finds on -Asked, which writes no + counts; the older one writes a number there on every statement. So a record whose `capacity` is + a number has a level an older lane.ps1 stated. This hook shows that level's in-flight count, + withholds its free count, labels it OLD-WRITER and asks the lane to restate with the current + script. The number is read as a marker of who stated the counts, never as a cap. A record whose + `capacity` is null or absent is read as stated. + + EVERY NUMBER IS A LANE'S OWN QUOTE. A missing file, an unreadable file, a stale number, a + superseded number, a roster shortfall -- each is said aloud, never turned into a count. + + THE DISPATCHER WRITES NOTHING THIS HOOK READS. It never reads mail, receipts, dispatch traffic, + or claim counts to derive occupancy. The failure it replaces, measured 2026-08-23: a lane held NINE claims while reporting ONE active item and THREE free slots. A claim-counting gauge would have reported both lanes full for the @@ -39,7 +52,6 @@ $ErrorActionPreference = 'Stop' $EXPECTED_LANES = 2 $STALE_TURNS = 6 $STALE_MIN = 30 -$SERIAL_MIN_INFLIGHT = 2 $LANE_PROMPT_TURNS = 3 $LANE_PROMPT_MIN = 15 $SEAT_DIR_FLOOR = 5 @@ -64,8 +76,22 @@ function Read-Json([string]$Path) { } function Get-AgeMin($utc) { + # ConvertFrom-Json hands back statedUtc as a [datetime] with Kind=Utc. [datetime]::Parse on that + # stringifies it with no zone, re-reads it as local, and adds the offset again, so a stamp 45 + # minutes old read as hours in the future and the STALE and re-prompt checks never fired. + # lane.ps1 documents the same trap at its statedUtc guard; this mirrors its fix. if (-not $utc) { return $null } - try { return [math]::Round(([datetime]::UtcNow - ([datetime]::Parse($utc)).ToUniversalTime()).TotalMinutes, 0) } + try { + $at = if ($utc -is [datetime]) { + if ($utc.Kind -eq 'Local') { $utc.ToUniversalTime() } + else { [datetime]::SpecifyKind($utc, [System.DateTimeKind]::Utc) } + } else { + [datetime]::Parse([string]$utc, [cultureinfo]::InvariantCulture, + [System.Globalization.DateTimeStyles]::AdjustToUniversal -bor + [System.Globalization.DateTimeStyles]::AssumeUniversal) + } + return [math]::Round(([datetime]::UtcNow - $at).TotalMinutes, 0) + } catch { return $null } } @@ -79,18 +105,25 @@ function Test-Has($rec, [string]$name) { return ($rec.PSObject.Properties.Name -contains $name) } +function Test-OldWriter($rec) { + # See the header: a number in `capacity` marks a record an older lane.ps1 wrote. Only whether it + # is null is read, never its value. + return ($null -ne $rec) -and ($rec -isnot [string]) -and (Test-Has $rec 'capacity') -and + ($null -ne $rec.capacity) +} + function Get-LaneVerdict($rec, [string]$boxKey) { - # One occupancy word and one confidence word, or a single non-answer word. `greenable` is the ONLY - # thing the caller may use to stay quiet, and it is set in exactly one place. + # One state word and one confidence word, or a single non-answer word. A readable level is IDLE + # when nothing is in flight and STATED otherwise, whatever its numbers: no count makes a lane + # full. A level an older lane.ps1 wrote is OLD-WRITER and carries no free count (see the header). $o = [ordered]@{ box = $boxKey; occ = ''; conf = ''; free = $null; inflight = $null - cap = 4; ageMin = $null; turns = $null; note = ''; greenable = $false } + ageMin = $null; turns = $null; note = '' } if ($null -eq $rec) { $o.occ = 'NO-RECORD'; return [pscustomobject]$o } if ($rec -is [string] -and $rec -eq 'UNREADABLE') { $o.occ = 'UNREADABLE'; return [pscustomobject]$o } $free = if (Test-Has $rec 'statedFree') { $rec.statedFree } else { $null } $inf = if (Test-Has $rec 'statedInFlight') { $rec.statedInFlight } else { $null } - if ((Test-Has $rec 'capacity') -and $rec.capacity) { $o.cap = [int]$rec.capacity } if (Test-Has $rec 'statedNote') { $o.note = [string]$rec.statedNote } if (Test-Has $rec 'turnsSinceStated') { $o.turns = [int]$rec.turnsSinceStated } $statedUtc = if (Test-Has $rec 'statedUtc') { $rec.statedUtc } else { $null } @@ -101,19 +134,13 @@ function Get-LaneVerdict($rec, [string]$boxKey) { $o.occ = if ($pc -gt 0) { 'NEVER-STATED' } else { 'NO-RECORD' } return [pscustomobject]$o } - if ($null -eq $free) { $o.occ = 'UNREADABLE'; return [pscustomobject]$o } - - $o.free = [int]$free - if ($null -ne $inf) { $o.inflight = [int]$inf } - - if ($o.free -gt 0) { - $o.occ = 'OPEN' - } elseif ($null -eq $o.inflight) { - $o.occ = 'UNREADABLE'; return [pscustomobject]$o - } elseif ($o.inflight -lt $SERIAL_MIN_INFLIGHT) { - $o.occ = 'SERIAL' - } else { - $o.occ = 'AT-CAPACITY' + if ($null -eq $free -or $null -eq $inf) { $o.occ = 'UNREADABLE'; return [pscustomobject]$o } + + $o.inflight = [int]$inf + if (Test-OldWriter $rec) { $o.occ = 'OLD-WRITER' } + else { + $o.free = [int]$free + $o.occ = if ($o.inflight -eq 0) { 'IDLE' } else { 'STATED' } } $tipMoved = (Test-Has $rec 'tipNow') -and (Test-Has $rec 'tipAtStated') -and @@ -126,52 +153,64 @@ function Get-LaneVerdict($rec, [string]$boxKey) { ($null -ne $o.ageMin -and $o.ageMin -gt $STALE_MIN)) { $o.conf = 'STALE' } else { $o.conf = 'FRESH' } - $o.greenable = ($o.occ -eq 'AT-CAPACITY' -and $o.conf -eq 'FRESH') return [pscustomobject]$o } $BLIND = @' - CANNOT SEE: whether the number is right, whether an item concluded since it was typed, whether a hold - makes those slots undispatchable, or anything about a lane that writes no record. Every number above - is a QUOTE from that lane, never a measurement, and never derived from claims. OPEN clears when the - LANE RESTATES, not when you dispatch -- if you dispatched and OPEN persists, that lane has not taken - a turn yet. + CANNOT SEE: whether the number is right, whether an item concluded since it was typed, whether a + Steward hold is in force, or anything about a lane that writes no record. Every number above is a + QUOTE from that lane, never a measurement, and never derived from claims. A number changes when the + LANE RESTATES, not when you dispatch. There is no item ceiling, so no count above is a reason to + stop briefing; a hold is, and this hook does not read holds. '@ if ($SelfTest) { $fail = @() - function New-Rec($f, $i, $t1, $t2, $turns) { + # The current lane.ps1 writes capacity as null; pass 4 for a record an older one wrote. + function New-Rec($f, $i, $t1, $t2, $turns, $cap = $null) { [pscustomobject]@{ statedFree = $f; statedInFlight = $i statedUtc = ([datetime]::UtcNow.ToString('yyyy-MM-ddTHH:mm:ssZ')) tipAtStated = $t1; tipNow = $t2; claimHashAtStated = 'a'; claimHashNow = 'a' - turnsSinceStated = $turns; capacity = 4; statedNote = ''; promptCount = 1 } + turnsSinceStated = $turns; capacity = $cap; statedNote = ''; promptCount = 1 } } $v = Get-LaneVerdict (New-Rec 3 1 'x' 'x' 0) 'b1' - if ($v.occ -ne 'OPEN') { $fail += "3 free must be OPEN, got $($v.occ)" } - if ($v.greenable) { $fail += 'an OPEN lane must never be greenable' } + if ($v.occ -ne 'STATED' -or $v.free -ne 3 -or $v.inflight -ne 1) { $fail += "3 free / 1 in flight must be STATED 3/1, got $($v.occ) $($v.free)/$($v.inflight)" } - $v = Get-LaneVerdict (New-Rec 0 3 'x' 'x' 0) 'b2' - if (-not $v.greenable) { $fail += "0 free / 3 in flight fresh must be greenable, got $($v.occ)/$($v.conf)" } + # The retired ceiling's own case: an old record naming capacity 4, with 0 free and 4 in flight. + $v = Get-LaneVerdict (New-Rec 0 4 'x' 'x' 0 4) 'b2' + if ($v.occ -ne 'OLD-WRITER' -or $v.conf -ne 'FRESH') { $fail += "0 free / 4 in flight on capacity 4 must be OLD-WRITER FRESH, never full, got $($v.occ)/$($v.conf)" } + if ($v.PSObject.Properties.Name -contains 'cap') { $fail += 'a verdict must carry no capacity' } + + # An older lane.ps1's free count includes empty slots, so it is withheld rather than shown as held. + $v = Get-LaneVerdict (New-Rec 2 2 'x' 'x' 0 4) 'b9' + if ($v.occ -ne 'OLD-WRITER' -or $null -ne $v.free -or $v.inflight -ne 2) { $fail += "capacity 4, 2 free / 2 in flight must be OLD-WRITER with no free count and 2 in flight, got $($v.occ) $($v.free)/$($v.inflight)" } + + # The control: the same counts with capacity null are the current writer's, so 2 is held items. + $v = Get-LaneVerdict (New-Rec 2 2 'x' 'x' 0) 'b10' + if ($v.occ -ne 'STATED' -or $v.free -ne 2 -or $v.inflight -ne 2) { $fail += "capacity null, 2 free / 2 in flight must be STATED 2/2, got $($v.occ) $($v.free)/$($v.inflight)" } $v = Get-LaneVerdict $null 'b3' - if ($v.occ -ne 'NO-RECORD' -or $v.greenable) { $fail += 'a missing record must be NO-RECORD and never greenable' } + if ($v.occ -ne 'NO-RECORD') { $fail += 'a missing record must be NO-RECORD' } $v = Get-LaneVerdict 'UNREADABLE' 'b4' - if ($v.occ -ne 'UNREADABLE' -or $v.greenable) { $fail += 'an unreadable record must never be greenable' } + if ($v.occ -ne 'UNREADABLE') { $fail += 'an unreadable record must be UNREADABLE' } $v = Get-LaneVerdict (New-Rec 0 3 'aaa' 'bbb' 0) 'b5' - if ($v.conf -ne 'SUPERSEDED' -or $v.greenable) { $fail += "a moved tip must be SUPERSEDED, got $($v.conf)" } + if ($v.conf -ne 'SUPERSEDED') { $fail += "a moved tip must be SUPERSEDED, got $($v.conf)" } $v = Get-LaneVerdict (New-Rec 0 3 'x' 'x' 99) 'b6' - if ($v.conf -ne 'STALE' -or $v.greenable) { $fail += "99 turns must be STALE, got $($v.conf)" } + if ($v.conf -ne 'STALE') { $fail += "99 turns must be STALE, got $($v.conf)" } - $v = Get-LaneVerdict (New-Rec 0 1 'x' 'x' 0) 'b7' - if ($v.occ -ne 'SERIAL' -or $v.greenable) { $fail += "0 free / 1 in flight must be SERIAL, got $($v.occ)" } + $v = Get-LaneVerdict (New-Rec 2 $null 'x' 'x' 0) 'b7' + if ($v.occ -ne 'UNREADABLE') { $fail += "a level with no in-flight count must be UNREADABLE, got $($v.occ)" } + + $v = Get-LaneVerdict (New-Rec 0 0 'x' 'x' 0) 'b8' + if ($v.occ -ne 'IDLE') { $fail += "0 in flight must be IDLE, got $($v.occ)" } if ($fail.Count) { $fail | ForEach-Object { Write-Host "FAIL: $_" }; exit 1 } - Write-Host 'lane-level self-test: 7 assertions pass, 6 of them asserting a state that must NOT be greenable.' + Write-Host 'lane-level self-test: 10 cases pass, and none of them can read a lane as full.' exit 0 } @@ -244,6 +283,21 @@ $branch = (& git rev-parse --abbrev-ref HEAD 2>$null) if ($leaf -match '^builder') { $rec = Read-LaneFor $cwd (Get-CoordHost) + # BOTH NAGS NAME THE lane.ps1 BESIDE THIS HOOK. The copy in this worktree may be the older one, + # and a restatement through it writes a level the OLD-WRITER nag below then asks about again. + $current = Join-Path (Split-Path $PSScriptRoot -Parent) 'coord\lane.ps1' + $stated = (Test-OldWriter $rec) -and (Test-Has $rec 'statedUtc') -and $rec.statedUtc + if ($stated) { + # Asked every turn until it restates, however fresh the level: its free count is not held + # items. A record with no stated level holds no count, so the unstated nag below covers it. + Say '[lane] Your lane level was written by an OLDER lane.ps1, whose free count includes empty' + Say ' slots under the retired ceiling, so it is not shown as held items. Restate it with the' + Say ' current script, from this worktree:' + Say " pwsh -NoProfile -File `"$current`" -Free -InFlight -Note `"`"" + Say ' FREE MEANS ITEMS YOU HOLD AND ARE NOT WORKING: built-and-awaiting-land, blocked, parked.' + Say ' There is no item ceiling, so count no empty slots.' + Emit + } $need = ($null -eq $rec) -or ($rec -is [string]) if (-not $need) { $t = if (Test-Has $rec 'turnsSinceStated') { [int]$rec.turnsSinceStated } else { 99 } @@ -252,9 +306,10 @@ if ($leaf -match '^builder') { } if ($need) { Say '[lane] Your lane level is unstated or stale. State it now, it takes five seconds:' - Say ' pwsh -NoProfile -File scripts\coord\lane.ps1 -Free -InFlight -Note ""' + Say " pwsh -NoProfile -File `"$current`" -Free -InFlight -Note `"`"" Say ' FREE MEANS BY WORK, NOT BY CLAIMS. Built-and-awaiting-land is FREE. Blocked is FREE.' - Say ' Parked on a condition is FREE. Nobody else can write this number for you.' + Say ' Parked on a condition is FREE. There is no item ceiling, so count no empty slots.' + Say ' Nobody else can write this number for you.' } Emit } @@ -316,7 +371,8 @@ foreach ($c in $cands) { # so a lane silent for hours is one nobody is dispatching to. Measured today the live lanes sat at # 0.1h and 0.2h and the next candidate at 10.2h -- a real gap, not a threshold fitted to noise. # If this ever DROPS a live lane, `discovered` falls below `expected` and the ROSTER DRIFT block - # fires. Narrowing can therefore cost noise; it can never manufacture a green. + # fires. Narrowing can therefore cost noise; while the expected count is right, it cannot hide + # a lane silently. if (((([datetime]::UtcNow) - $c.at).TotalHours) -gt 6) { continue } $lanes += $c } @@ -327,7 +383,7 @@ foreach ($c in $cands) { # # THE ASYMMETRY IS THE WHOLE POINT: a lane is dropped ONLY when the call is Evaluable AND says dead. # When Evaluable is false the lane is KEPT and marked LIVENESS-UNKNOWN, which speaks. An unevaluated -# fence is not a passed fence, so doubt can shrink the noise but can never manufacture a green. +# fence is not a passed fence, so doubt can shrink the noise but can never hide a lane silently. $live = @() $unknown = @() try { @@ -352,21 +408,11 @@ $verdicts = @() foreach ($l in $lanes) { $verdicts += Get-LaneVerdict (Read-LaneFor $l.worktree $l.host) $l.box } $discovered = $verdicts.Count -$notGreen = @($verdicts | Where-Object { -not $_.greenable }).Count $unknownLive = @($verdicts | Where-Object { $unknownBoxes -contains $_.box }).Count -# A lane whose liveness could not be evaluated blocks green even if it states itself full: the -# statement may be from a session that is gone. -$green = ($discovered -eq $EXPECTED_LANES) -and ($discovered -gt 0) -and ($notGreen -eq 0) -and ($unknownLive -eq 0) - -if ($green) { - $bits = ($verdicts | ForEach-Object { - "$($_.box) $($_.free) free / $($_.inflight) in flight of $($_.cap) (stated $(Format-Age $_.ageMin) ago)" }) -join ' | ' - Say "[lane] BOTH LANES AT CAPACITY BY THEIR OWN STATEMENT: $bits. Quoted, not measured." - Emit -} -$below = @($verdicts | Where-Object { $_.occ -ne 'AT-CAPACITY' }).Count -Say "[lane] $below of $discovered LANES BELOW CAPACITY BY THEIR OWN STATEMENT." +# NO VERDICT OF FULLNESS, AND NO SILENCE BOUGHT BY ONE. There is no item ceiling (see the header), so +# the counts are reported as quoted and the hook speaks on every dispatcher turn. +Say "[lane] $discovered LANES, EACH NUMBER BY ITS OWN STATEMENT." foreach ($v in ($verdicts | Sort-Object box)) { $lbl = ("$($v.occ) $($v.conf)").Trim() $pad = '{0,-28} {1,-20}' -f $v.box, $lbl @@ -374,24 +420,28 @@ foreach ($v in ($verdicts | Sort-Object box)) { 'NO-RECORD' { Say " $pad never wrote a level, and its own hook never prompted it" Say (' {0,-28} {1,-20} either. That points at the HOOK, not the lane. Nothing is' -f '', '') - Say (' {0,-28} {1,-20} known here: not zero, and not full.' -f '', '') } + Say (' {0,-28} {1,-20} known here, not even zero.' -f '', '') } 'NEVER-STATED' { Say " $pad prompted on its own turns and never answered. The hook runs" Say (' {0,-28} {1,-20} there; the number is not coming. Ask the lane directly.' -f '', '') } 'UNREADABLE' { - Say " $pad its level file did not parse. NO verdict for this lane, and" + Say " $pad its level file did not parse, or lacks a count. NO verdict, and" Say (' {0,-28} {1,-20} this is NOT a pass.' -f '', '') } - 'SERIAL' { - Say " $pad $($v.free) free / $($v.inflight) in flight of $($v.cap), stated $(Format-Age $v.ageMin) ago." - Say (' {0,-28} {1,-20} OCCUPANCY CANNOT SEE A SERIAL LANE -- it reads as full.' -f '', '') } + 'OLD-WRITER' { + $n = if ($v.note) { " Note: `"$($v.note)`"" } else { '' } + Say " $pad $($v.inflight) in flight, stated $(Format-Age $v.ageMin) and $($v.turns) lane-turns ago.$n" + Say (' {0,-28} {1,-20} WRITTEN BY AN OLDER lane.ps1: its free count includes empty' -f '', '') + Say (' {0,-28} {1,-20} slots, so it is not shown as held items. Ask the lane to' -f '', '') + Say (' {0,-28} {1,-20} restate with a current lane.ps1; its own worktree may hold' -f '', '') + Say (' {0,-28} {1,-20} the older copy. Its own hook asks only in a builder* worktree.' -f '', '') } default { $n = if ($v.note) { " Note: `"$($v.note)`"" } else { '' } - Say " $pad $($v.free) free / $($v.inflight) in flight of $($v.cap), stated $(Format-Age $v.ageMin) and $($v.turns) lane-turns ago.$n" } + Say " $pad $($v.inflight) in flight, $($v.free) free by work, stated $(Format-Age $v.ageMin) and $($v.turns) lane-turns ago.$n" } } if ($v.conf -eq 'SUPERSEDED') { Say (' {0,-28} {1,-20} that lane has committed or changed claims since it stated' -f '', '') Say (' {0,-28} {1,-20} that number, so the number is NOT current. Ask it.' -f '', '') } - if ($v.conf -eq 'STALE') { Say (' {0,-28} {1,-20} STALE IS NOT FULL.' -f '', '') } + if ($v.conf -eq 'STALE') { Say (' {0,-28} {1,-20} STALE: the number may no longer hold. Ask it.' -f '', '') } if ($unknownBoxes -contains $v.box) { Say (' {0,-28} {1,-20} LIVENESS-UNKNOWN: too few activity signals could be read, so I' -f '', '') Say (' {0,-28} {1,-20} cannot say this lane is alive. An unevaluated fence is not a' -f '', '') diff --git a/tests/test_coord_lane_no_ceiling.py b/tests/test_coord_lane_no_ceiling.py new file mode 100644 index 0000000000..583aded212 --- /dev/null +++ b/tests/test_coord_lane_no_ceiling.py @@ -0,0 +1,354 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (C) 2026 MessageFoundry Foundation, LLC and contributors +"""The lane tooling carries no item ceiling (owner ruling 2026-10-26). + +The ruling is stated once, in the header of ``scripts/coord/lane.ps1``. That script defaulted +``-Capacity`` to 4 and wrote it into every lane record, and ``scripts/hooks/lane-level.ps1`` read a +lane stating no free items as AT CAPACITY. + +These tests fail if a default cap of 4 comes back in either script: + +1. a statement records ``capacity`` as null, with no upper bound on its counts, and prints no + ceiling; a 4 carried on an old record is replaced with null by the next statement, and kept by + ``-Asked``, which writes no counts. The key stays, because an older lane.ps1 on another branch + assigns it, and that throws when the key is missing; +2. ``-Capacity`` is not a parameter any more, so a caller cannot reintroduce a cap by passing one; +3. the dispatcher arm of the hook reports a lane that states 0 free and 4 in flight, on a record + still naming capacity 4, with its in-flight count and with no capacity verdict. Against the + scripts before this change it printed ``AT-CAPACITY`` and ``of 4``; +4. a record whose ``capacity`` is a number was written by an older lane.ps1, whose free count + includes empty slots. The hook labels it OLD-WRITER, withholds that free count rather than show + it as held items, and asks the lane to restate; the same counts with ``capacity`` null are shown; +5. the hook's own self-test passes; +6. a builder lane whose level is 45 minutes old is asked to restate. The hook read every stamp + hours in the future on a machine west of UTC until this change, so no level aged out there. +""" + +from __future__ import annotations + +import json +import os +import shutil +import subprocess +import time +from pathlib import Path +from typing import Any + +import pytest + +ROOT = Path(__file__).resolve().parents[1] +LANE = ROOT / "scripts" / "coord" / "lane.ps1" +HOOK = ROOT / "scripts" / "hooks" / "lane-level.ps1" +MAIL_KEY = ROOT / "scripts" / "coord" / "mail-key.ps1" +TIMEOUT = 90 + +pytestmark = pytest.mark.skipif( + shutil.which("pwsh") is None or os.name != "nt", + reason="the coordination scripts need pwsh on Windows", +) + + +def _env(root: Path | None = None) -> dict[str, str]: + env = { + k: v + for k, v in os.environ.items() + if k not in ("MEFOR_COORD_ROOT", "CLAUDE_CODE_SESSION_ID") + } + if root is not None: + env["MEFOR_COORD_ROOT"] = str(root) + return env + + +def _ps( + script: Path, cwd: Path, *args: str, root: Path | None = None, stdin: str = "" +) -> subprocess.CompletedProcess[str]: + return subprocess.run( + ["pwsh", "-NoProfile", "-NonInteractive", "-File", str(script), *args], + cwd=str(cwd), + input=stdin, + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + timeout=TIMEOUT, + check=False, + env=_env(root), + ) + + +def _git_repo(path: Path) -> Path: + path.mkdir(parents=True, exist_ok=True) + for args in ( + ["init", "-q", "-b", "main"], + ["config", "user.email", "t@example.invalid"], + ["config", "user.name", "t"], + ["commit", "-q", "--allow-empty", "-m", "base"], + ): + subprocess.run(["git", "-C", str(path), *args], check=True, timeout=TIMEOUT) + return path + + +def _lane_record(repo: Path) -> Path: + files = sorted((repo / ".git" / "mefor-coord" / "lanes").glob("*.json")) + assert len(files) == 1, f"expected exactly one lane record, found {[p.name for p in files]}" + return files[0] + + +# ------------------------------------------------------------------------------------- lane.ps1 + + +def test_a_statement_records_no_capacity_and_prints_no_ceiling(tmp_path: Path) -> None: + repo = _git_repo(tmp_path / "repo") + # 150 is past the old ValidateRange(0, 99) on both counts: no upper bound may come back either. + proc = _ps(LANE, repo, "-Free", "150", "-InFlight", "150", "-Note", "n") + assert proc.returncode == 0, proc.stdout + proc.stderr + rec = json.loads(_lane_record(repo).read_text(encoding="utf-8")) + assert "capacity" in rec and rec["capacity"] is None, rec + # The control: the statement itself was recorded, counts and all. + assert rec["statedFree"] == 150 and rec["statedInFlight"] == 150, rec + assert "150 free / 150 in flight," in proc.stdout, proc.stdout + assert " of " not in proc.stdout.split("stated")[0], proc.stdout + + +def test_a_negative_count_is_still_refused(tmp_path: Path) -> None: + """The control for the unbounded range: the lower bound still holds.""" + repo = _git_repo(tmp_path / "repo") + proc = _ps(LANE, repo, "-Free", "-1", "-InFlight", "0") + assert proc.returncode != 0, proc.stdout + assert not (repo / ".git" / "mefor-coord" / "lanes").exists(), "a refused call wrote a lane" + + +def test_a_capacity_left_on_an_old_record_is_nulled_only_by_a_statement(tmp_path: Path) -> None: + """``-Asked`` writes no counts, so it keeps the marker of who stated the counts there.""" + repo = _git_repo(tmp_path / "repo") + assert _ps(LANE, repo, "-Free", "1", "-InFlight", "1").returncode == 0 + record = _lane_record(repo) + old: dict[str, Any] = json.loads(record.read_text(encoding="utf-8")) + old["capacity"] = 4 + record.write_text(json.dumps(old), encoding="utf-8") + # The control: -Show is a strict read and prints the old record as it is. + show = _ps(LANE, repo, "-Show") + assert show.returncode == 0, show.stderr + assert json.loads(show.stdout)["capacity"] == 4 + + for args, kept in ((("-Asked",), 4), (("-Free", "0", "-InFlight", "5"), None)): + record.write_text(json.dumps(old), encoding="utf-8") + proc = _ps(LANE, repo, *args) + assert proc.returncode == 0, (args, proc.stdout + proc.stderr) + new = json.loads(record.read_text(encoding="utf-8")) + assert "capacity" in new and new["capacity"] == kept, (args, new) + # The control: both calls wrote. -Asked stamps its time and leaves the counts; a statement + # writes its counts. + if kept is None: + assert new["statedInFlight"] == 5, (args, new) + else: + assert new["askedByDispatcherUtc"] and new["statedInFlight"] == 1, (args, new) + + +def test_asked_adds_a_missing_capacity_key_as_null(tmp_path: Path) -> None: + """An older lane.ps1 assigns ``capacity`` and throws when the key is missing, so -Asked adds it.""" + repo = _git_repo(tmp_path / "repo") + assert _ps(LANE, repo, "-Free", "1", "-InFlight", "1").returncode == 0 + record = _lane_record(repo) + rec: dict[str, Any] = json.loads(record.read_text(encoding="utf-8")) + del rec["capacity"] + record.write_text(json.dumps(rec), encoding="utf-8") + assert _ps(LANE, repo, "-Asked").returncode == 0 + new = json.loads(record.read_text(encoding="utf-8")) + assert "capacity" in new and new["capacity"] is None, new + assert new["askedByDispatcherUtc"], new + + +def test_capacity_is_no_longer_a_parameter(tmp_path: Path) -> None: + repo = _git_repo(tmp_path / "repo") + proc = _ps(LANE, repo, "-Free", "0", "-InFlight", "4", "-Capacity", "4") + assert proc.returncode != 0, proc.stdout + assert "Capacity" in proc.stderr, proc.stderr + assert not (repo / ".git" / "mefor-coord" / "lanes").exists(), "a refused call wrote a lane" + + +# --------------------------------------------------------------------------------- lane-level.ps1 + + +def _box_key(path: Path) -> str: + """The box key from the real mail-key.ps1, never a Python copy of it.""" + proc = subprocess.run( + [ + "pwsh", + "-NoProfile", + "-NonInteractive", + "-Command", + f". '{MAIL_KEY}'; ConvertTo-BoxKey -Path $env:BK_PATH", + ], + capture_output=True, + text=True, + timeout=TIMEOUT, + check=True, + env=dict(os.environ, BK_PATH=str(path)), + ) + return proc.stdout.strip() + + +def _plant_seat(seats: Path, box: str, worktree: Path | str, seat: str) -> None: + d = seats / box + d.mkdir(parents=True, exist_ok=True) + rec = { + "schema": 1, + "asOf": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), + "lifecycle": "open", + "boxKey": box, + "worktree": str(worktree), + "sessionKey": f"s-{box}", + "seat": seat, + "branch": "main", + "claims": [], + "allocations": [], + "handoff": None, + } + (d / f"s-{box}.json").write_text(json.dumps(rec), encoding="utf-8") + + +def _dispatcher_report(tmp_path: Path, lane_fields: dict[str, Any]) -> str: + """The dispatcher arm's text for one live builder lane holding ``lane_fields``.""" + shared = tmp_path / "shared" / "mefor-coord" + seats = shared / "seats" + lanes = shared / "lanes" + lanes.mkdir(parents=True) + dispatcher = _git_repo(tmp_path / "dispatcher-lane") + tree = tmp_path / "builder-tree" + tree.mkdir() + + box = _box_key(tree) + _plant_seat(seats, box, tree, "builder") + # The hook reads no verdict below a floor of five seat directories. These name no live tree. + for i in range(4): + _plant_seat(seats, f"filler-{i:04d}", "C:/no/such/tree", "manager") + # Stated just now with nothing moved since. + lane: dict[str, Any] = { + "v": 1, + "statedUtc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), + "turnsSinceStated": 0, + "tipAtStated": "x", + "tipNow": "x", + "claimHashAtStated": "a", + "claimHashNow": "a", + **lane_fields, + } + (lanes / f"{box}.json").write_text(json.dumps(lane), encoding="utf-8") + + proc = _ps(HOOK, dispatcher, root=shared, stdin=json.dumps({"hook_event_name": "Stop"})) + assert proc.returncode == 0, proc.stderr + return str(json.loads(proc.stdout)["hookSpecificOutput"]["additionalContext"]) + + +OLD_WRITER = "WRITTEN BY AN OLDER lane.ps1" + + +def test_the_dispatcher_report_has_no_capacity_verdict(tmp_path: Path) -> None: + # The retired ceiling's own case, on a record written before the change: 0 free, 4 in flight, + # capacity 4. + text = _dispatcher_report( + tmp_path, + {"capacity": 4, "statedFree": 0, "statedInFlight": 4, "statedNote": "four in flight"}, + ) + + # The control: the lane was discovered and its record read, before and after this change alike. + assert 'Note: "four in flight"' in text, text + # The ceiling checks come first, so against the old scripts they are what fails. + assert "capacity" not in text.lower(), text + assert "of 4" not in text, text + assert "AT" + "-CAPACITY" not in text, text + assert "4 in flight" in text, text + # A level stated just now is not in the future. West of UTC the old age read said -300m here. + assert "stated -" not in text, text + + +@pytest.mark.parametrize(("capacity", "old_writer"), [(4, True), (None, False)]) +def test_an_older_writers_free_count_is_not_shown_as_held_items( + tmp_path: Path, capacity: int | None, old_writer: bool +) -> None: + """Capacity null is the control: the same counts from the current writer are held items.""" + text = _dispatcher_report( + tmp_path, {"capacity": capacity, "statedFree": 2, "statedInFlight": 2, "statedNote": "n"} + ) + # Both arms read the record and show its in-flight count. + assert 'Note: "n"' in text, text + assert "2 in flight" in text, text + assert (OLD_WRITER in text) is old_writer, text + assert ("OLD-WRITER" in text) is old_writer, text + assert ("2 free by work" in text) is not old_writer, text + assert "capacity" not in text.lower(), text + + +def test_the_hook_self_test_passes(tmp_path: Path) -> None: + proc = _ps(HOOK, tmp_path, "-SelfTest") + assert proc.returncode == 0, proc.stdout + proc.stderr + assert "none of them can read a lane as full" in proc.stdout, proc.stdout + + +NAG = "Your lane level is unstated or stale" + + +@pytest.mark.parametrize(("minutes_ago", "nags"), [(0, False), (45, True)]) +def test_a_builder_lane_is_asked_again_once_its_level_ages( + tmp_path: Path, minutes_ago: int, nags: bool +) -> None: + """The 0-minute case is the control: the nag below is the age's, not the record's shape's.""" + shared = tmp_path / "shared" / "mefor-coord" + (shared / "lanes").mkdir(parents=True) + builder = _git_repo(tmp_path / "builder-lane") + stamp = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(time.time() - minutes_ago * 60)) + lane = {"v": 1, "statedFree": 0, "statedInFlight": 1, "statedUtc": stamp, "turnsSinceStated": 0} + (shared / "lanes" / f"{_box_key(builder)}.json").write_text(json.dumps(lane), encoding="utf-8") + + proc = _ps(HOOK, builder, root=shared, stdin=json.dumps({"hook_event_name": "Stop"})) + assert proc.returncode == 0, proc.stderr + assert (NAG in proc.stdout) is nags, proc.stdout + + +OLD_NAG = "Your lane level was written by an OLDER lane.ps1" + + +def _builder_report(tmp_path: Path, lane_fields: dict[str, Any]) -> str: + """The builder arm's text for its own lane record holding ``lane_fields``.""" + shared = tmp_path / "shared" / "mefor-coord" + (shared / "lanes").mkdir(parents=True) + builder = _git_repo(tmp_path / "builder-lane") + lane = {"v": 1, "turnsSinceStated": 0, **lane_fields} + (shared / "lanes" / f"{_box_key(builder)}.json").write_text(json.dumps(lane), encoding="utf-8") + + proc = _ps(HOOK, builder, root=shared, stdin=json.dumps({"hook_event_name": "Stop"})) + assert proc.returncode == 0, proc.stderr + out = proc.stdout.strip() + return str(json.loads(out)["hookSpecificOutput"]["additionalContext"]) if out else "" + + +@pytest.mark.parametrize(("capacity", "old_writer"), [(4, True), (None, False)]) +def test_a_builder_lane_on_an_older_writers_level_is_asked_to_restate( + tmp_path: Path, capacity: int | None, old_writer: bool +) -> None: + """A fresh level, so only the writer differs; capacity null is the control and is not asked.""" + stamp = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) + text = _builder_report( + tmp_path, + {"capacity": capacity, "statedFree": 2, "statedInFlight": 2, "statedUtc": stamp}, + ) + assert (OLD_NAG in text) is old_writer, text + assert NAG not in text, text + if old_writer: + # It names the lane.ps1 beside the hook, not the one in the worktree, which may be older. + assert f'"{LANE}"' in text, text + + +def test_an_older_writers_record_with_no_level_gets_the_unstated_nag(tmp_path: Path) -> None: + """An older ``-Asked`` record holds no count, so there is no free count to call slot-inclusive. + + The unstated nag also names the lane.ps1 beside the hook: restating through the worktree's own + copy, if that is the older one, would write a level the OLD-WRITER nag asks about again. + """ + text = _builder_report( + tmp_path, {"capacity": 4, "statedFree": None, "statedInFlight": None, "statedUtc": None} + ) + assert NAG in text, text + assert OLD_NAG not in text, text + assert f'"{LANE}"' in text, text diff --git a/tests/tooling_manifest.txt b/tests/tooling_manifest.txt index 83a79810ce..9c363219af 100644 --- a/tests/tooling_manifest.txt +++ b/tests/tooling_manifest.txt @@ -110,6 +110,7 @@ tests/test_coord_cloud_refusal.py tests/test_coord_handoff_clear.py tests/test_coord_handoff_pointer.py tests/test_coord_handoff_report.py +tests/test_coord_lane_no_ceiling.py tests/test_coord_lane_statement.py tests/test_coord_lock.py tests/test_coord_migrate.py