scripts/internal/continuous-co-review/co-review-lineage-lease.ps1
|
# T019 step 6 piece 2 — the atomically-acquired per-lineage review LEASE (maintainer design 2026-07-13). # # One active lease per lineage. The GENERATION is the IMMUTABLE reviewed-tree digest (never a timestamp, never # "latest fire wins"). The atomic winner is the exclusive file creator (FileMode::CreateNew); a failed acquire for # the same lineage SUPPRESSES the spawn (the caller then consumes neither provider spend nor a review round). # Ownership + crash recovery are explicit: the lease persists lineage_id, generation, run_id, a random owner token, # the owner PID + process-START identity (PID-reuse protection), and the acquisition time. Release is OWNER-ONLY # (token + generation must match). A stale lease is RECLAIMED only when its owner is PROVABLY DEAD (or by explicit # remediation) - NEVER merely because wall-clock time elapsed. A newer tree arriving while the current tree is # under review is QUEUED as pending_tree (no second reviewer); when the owner completes, its now-stale result is # superseded and the pending newest tree becomes eligible for the next generation. # # Verdict authority is NOT lease ownership alone: Test-ContinuousCoReviewLeasePromotionAuthority requires ALL of # owner-match, generation==result-digest, the T019 identity joins, and not-superseded-by-current. # StrictMode-safe property read (self-contained so the module loads independently). function Get-ContinuousCoReviewLeaseProp { param([Parameter(Mandatory)][AllowNull()]$Object, [Parameter(Mandatory)][string]$Name) if ($null -eq $Object) { return $null } if ($Object -is [System.Collections.IDictionary]) { if ($Object.Contains($Name)) { return $Object[$Name] } else { return $null } } $p = $Object.PSObject.Properties[$Name] if ($null -ne $p) { return $p.Value } return $null } function Get-ContinuousCoReviewLineageLeaseDir { param([Parameter(Mandatory)][string]$RepoRoot) Join-Path $RepoRoot '.specrew/runtime/co-review-lineage-lease' } # HASH the lineage id for the filename so a hostile/odd lineage id cannot traverse paths or produce an invalid # filename (the raw lineage id is preserved INSIDE the lease record, only the filename is the hash). function Get-ContinuousCoReviewLineageLeasePath { param([Parameter(Mandatory)][string]$RepoRoot, [Parameter(Mandatory)][AllowEmptyString()][string]$LineageId) $bytes = [System.Text.Encoding]::UTF8.GetBytes([string]$LineageId) $hash = [System.BitConverter]::ToString([System.Security.Cryptography.SHA256]::HashData($bytes)).Replace('-', '').ToLowerInvariant() Join-Path (Get-ContinuousCoReviewLineageLeaseDir -RepoRoot $RepoRoot) ($hash + '.json') } # Process-START identity (PID + an OS-stable start marker) - the PID-reuse guard. '' when the process is gone. function Get-ContinuousCoReviewProcessStartIdentity { param([Parameter(Mandatory)][int]$ProcessId) try { if ($IsLinux) { # Process.StartTime on Linux is reconstructed independently by each observing process and can # differ by several milliseconds. That made a live lease owner look dead to another process, # permitting sequential/concurrent lease theft. /proc stat field 22 is the kernel start tick and # is identical across observers. The command field can contain spaces or ')' characters, so split # only after its final closing parenthesis; the remaining zero-based field 19 is stat field 22. $stat = [System.IO.File]::ReadAllText("/proc/$ProcessId/stat") $commandEnd = $stat.LastIndexOf(')') if ($commandEnd -lt 0) { return '' } $fields = @($stat.Substring($commandEnd + 1).Trim() -split '\s+') if ($fields.Count -le 19 -or $fields[19] -notmatch '^\d+$') { return '' } return ('{0}:linux-start-ticks:{1}' -f $ProcessId, $fields[19]) } $p = Get-Process -Id $ProcessId -ErrorAction Stop return ('{0}:{1}' -f $ProcessId, $p.StartTime.ToUniversalTime().Ticks) } catch { return '' } } # The recorded owner is ALIVE iff a process with that PID exists AND its start-identity matches (so a REUSED PID # now belonging to a DIFFERENT process reads as NOT the owner). A blank/zero pid or blank start id -> not alive. function Test-ContinuousCoReviewLeaseOwnerAlive { param([AllowNull()]$OwnerPid, [AllowEmptyString()][string]$ProcessStartId) if ($null -eq $OwnerPid) { return $false } $pidInt = 0 if (-not [int]::TryParse([string]$OwnerPid, [ref]$pidInt)) { return $false } if ($pidInt -le 0) { return $false } if ([string]::IsNullOrWhiteSpace($ProcessStartId)) { return $false } $current = Get-ContinuousCoReviewProcessStartIdentity -ProcessId $pidInt if ([string]::IsNullOrWhiteSpace($current)) { return $false } # no such process -> dead if ($IsLinux -and $ProcessStartId -match ('^{0}:\d+$' -f [regex]::Escape([string]$pidInt))) { # Conservative compatibility for leases written by the pre-/proc Linux format. Its StartTime value # cannot be compared reliably across observers. A still-existing PID is therefore treated as live # (safe suppression) until that legacy owner exits or explicit remediation occurs; never steal on an # uncertain identity during an in-place upgrade. return $true } return ($current -eq [string]$ProcessStartId) # same PID AND same start -> alive; else dead / PID-reused } function Get-ContinuousCoReviewLineageLease { param([Parameter(Mandatory)][string]$RepoRoot, [Parameter(Mandatory)][AllowEmptyString()][string]$LineageId) $path = Get-ContinuousCoReviewLineageLeasePath -RepoRoot $RepoRoot -LineageId $LineageId if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { return $null } try { return (Get-Content -LiteralPath $path -Raw -Encoding UTF8 -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop) } catch { return $null } } # Queue a NEWER reviewed tree as the lease's pending_tree (advisory hint for the next acquire). Guarded by the # expected generation so we never clobber a lease that has already advanced. Benign read-modify-write. function Set-ContinuousCoReviewLineageLeasePendingTree { param( [Parameter(Mandatory)][string]$RepoRoot, [Parameter(Mandatory)][AllowEmptyString()][string]$LineageId, [Parameter(Mandatory)][AllowEmptyString()][string]$PendingTree, [Parameter(Mandatory)][AllowEmptyString()][string]$ExpectGeneration ) $path = Get-ContinuousCoReviewLineageLeasePath -RepoRoot $RepoRoot -LineageId $LineageId $lease = Get-ContinuousCoReviewLineageLease -RepoRoot $RepoRoot -LineageId $LineageId if ($null -eq $lease) { return $false } if ([string](Get-ContinuousCoReviewLeaseProp -Object $lease -Name 'generation') -cne $ExpectGeneration) { return $false } try { $lease | Add-Member -NotePropertyName 'pending_tree' -NotePropertyValue ([string]$PendingTree) -Force ($lease | ConvertTo-Json -Depth 6) | Set-Content -LiteralPath $path -Encoding UTF8 return $true } catch { return $false } } # ATOMIC acquire. Returns @{ acquired; lease?; reason; existing?; reclaimed }. reasons: # acquired | duplicate-same-generation | queued-newer-tree | acquire-contended function Request-ContinuousCoReviewLineageLease { param( [Parameter(Mandatory)][string]$RepoRoot, [Parameter(Mandatory)][AllowEmptyString()][string]$LineageId, [Parameter(Mandatory)][AllowEmptyString()][string]$Generation, # = the reviewed-tree digest (immutable epoch) [Parameter(Mandatory)][AllowEmptyString()][string]$RunId, [datetime]$Now = [datetime]::UtcNow, [int]$AcquiringPid = $PID ) $dir = Get-ContinuousCoReviewLineageLeaseDir -RepoRoot $RepoRoot if (-not (Test-Path -LiteralPath $dir -PathType Container)) { $null = New-Item -ItemType Directory -Path $dir -Force } $path = Get-ContinuousCoReviewLineageLeasePath -RepoRoot $RepoRoot -LineageId $LineageId for ($attempt = 0; $attempt -lt 6; $attempt++) { # 1. ATOMIC WINNER: exclusively CREATE the lease file (throws if it already exists). $ownerToken = [guid]::NewGuid().ToString('N') $record = [ordered]@{ schema_version = '1.0' lineage_id = $LineageId generation = $Generation run_id = $RunId owner_token = $ownerToken pid = $AcquiringPid process_start_id = (Get-ContinuousCoReviewProcessStartIdentity -ProcessId $AcquiringPid) acquired_at = $Now.ToUniversalTime().ToString('o') pending_tree = $null } $json = ([pscustomobject]$record | ConvertTo-Json -Depth 6) $fs = $null $created = $false try { $fs = [System.IO.File]::Open($path, [System.IO.FileMode]::CreateNew, [System.IO.FileAccess]::Write, [System.IO.FileShare]::None) $bytes = [System.Text.Encoding]::UTF8.GetBytes($json) $fs.Write($bytes, 0, $bytes.Length) $fs.Flush() $created = $true } catch [System.IO.IOException] { $created = $false } # the file exists -> inspect the incumbent finally { if ($null -ne $fs) { $fs.Dispose() } } if ($created) { return [pscustomobject]@{ acquired = $true; lease = ([pscustomobject]$record); reason = 'acquired'; existing = $null; reclaimed = ($attempt -gt 0) } } # 2. A lease exists. Inspect it. $existing = Get-ContinuousCoReviewLineageLease -RepoRoot $RepoRoot -LineageId $LineageId if ($null -eq $existing) { continue } # vanished (a concurrent release) -> retry the atomic create # OWNER LIVENESS FIRST (review finding f3, run 20260714T215545754): the same-generation duplicate # answer is only honest for a LIVE incumbent - a supervisor that crashed while holding THIS generation # previously suppressed every same-generation retry forever (dead-owner reclaim was reachable only via # a DIFFERENT generation). $existingGen = [string](Get-ContinuousCoReviewLeaseProp -Object $existing -Name 'generation') $ownerPid = Get-ContinuousCoReviewLeaseProp -Object $existing -Name 'pid' $ownerStart = [string](Get-ContinuousCoReviewLeaseProp -Object $existing -Name 'process_start_id') $ownerAlive = Test-ContinuousCoReviewLeaseOwnerAlive -OwnerPid $ownerPid -ProcessStartId $ownerStart if ($existingGen -ceq $Generation -and $ownerAlive) { # SAME generation (same reviewed-tree epoch), LIVE owner = a duplicate fire. Do NOT acquire, do NOT spawn. return [pscustomobject]@{ acquired = $false; lease = $null; reason = 'duplicate-same-generation'; existing = $existing; reclaimed = $false } } if ($ownerAlive) { # LIVE owner reviewing an OLDER tree: NEVER steal, NEVER spawn a second reviewer. QUEUE our newer tree. $null = Set-ContinuousCoReviewLineageLeasePendingTree -RepoRoot $RepoRoot -LineageId $LineageId -PendingTree $Generation -ExpectGeneration $existingGen return [pscustomobject]@{ acquired = $false; lease = $null; reason = 'queued-newer-tree'; existing = $existing; reclaimed = $false } } # DEAD owner (crash) = ATOMIC RECLAIM (review finding f4, run 20260714T215545754): the old # check-then-delete raced - reclaimer A could re-read dead token X while reclaimer B deleted X and # CreateNew'd a valid replacement Y, then A's path-only delete removed Y and a second reviewer could # spawn against a live one. The reclaim is now CLAIM-BY-RENAME: File.Move of the exact lease path is # atomic and single-winner - only the process that owns the moved file may inspect and dispose of it. # - moved file carries the DEAD token -> dispose it; loop to the atomic CreateNew. # - moved file carries a DIFFERENT token (we displaced a concurrent replacement) -> move it BACK. # If the restore collides with an even newer CreateNew, the displaced lease is dropped with a LOUD # warn: its owner's completion degrades to non-authoritative/advisory (the lease file is the # authority), which is the SAFE direction - never two authoritative reviewers. $deadToken = [string](Get-ContinuousCoReviewLeaseProp -Object $existing -Name 'owner_token') $reclaimPath = $path + '.reclaim.' + [guid]::NewGuid().ToString('N') try { [System.IO.File]::Move($path, $reclaimPath) } catch { continue } # another reclaimer/creator won the path - retry the loop against the new state $movedToken = '' try { $moved = Get-Content -LiteralPath $reclaimPath -Raw -Encoding UTF8 | ConvertFrom-Json $movedToken = [string](Get-ContinuousCoReviewLeaseProp -Object $moved -Name 'owner_token') } catch { $movedToken = '' } if ($movedToken -ceq $deadToken) { # we claimed the exact dead incumbent - dispose and retry the atomic create. try { Remove-Item -LiteralPath $reclaimPath -Force -ErrorAction Stop } catch { $null = $_ } } else { # we displaced a CONCURRENT replacement - restore it. try { [System.IO.File]::Move($reclaimPath, $path) } catch { try { Remove-Item -LiteralPath $reclaimPath -Force -ErrorAction SilentlyContinue } catch { $null = $_ } [Console]::Error.WriteLine("[co-review] WARN LEASE_REPLACEMENT_DISPLACED a concurrent replacement lease for lineage '$LineageId' was displaced during reclaim and could not be restored; its owner's completion degrades to advisory (never two authoritative reviewers).") } } } return [pscustomobject]@{ acquired = $false; lease = $null; reason = 'acquire-contended'; existing = (Get-ContinuousCoReviewLineageLease -RepoRoot $RepoRoot -LineageId $LineageId); reclaimed = $false } } # OWNER-ONLY release: release ONLY when the owner token AND generation match the on-disk lease. Returns the queued # pending_tree (if any) so the caller can make it eligible for the next generation. function Complete-ContinuousCoReviewLineageLease { param( [Parameter(Mandatory)][string]$RepoRoot, [Parameter(Mandatory)][AllowEmptyString()][string]$LineageId, [Parameter(Mandatory)][AllowEmptyString()][string]$Generation, [Parameter(Mandatory)][AllowEmptyString()][string]$OwnerToken ) $existing = Get-ContinuousCoReviewLineageLease -RepoRoot $RepoRoot -LineageId $LineageId if ($null -eq $existing) { return [pscustomobject]@{ released = $false; reason = 'no-lease'; pending_tree = $null } } $eGen = [string](Get-ContinuousCoReviewLeaseProp -Object $existing -Name 'generation') $eTok = [string](Get-ContinuousCoReviewLeaseProp -Object $existing -Name 'owner_token') if (($eTok -ceq $OwnerToken) -and ($eGen -ceq $Generation) -and (-not [string]::IsNullOrWhiteSpace($OwnerToken))) { $pending = [string](Get-ContinuousCoReviewLeaseProp -Object $existing -Name 'pending_tree') $path = Get-ContinuousCoReviewLineageLeasePath -RepoRoot $RepoRoot -LineageId $LineageId try { Remove-Item -LiteralPath $path -Force -ErrorAction Stop } catch { return [pscustomobject]@{ released = $false; reason = 'delete-failed'; pending_tree = $pending } } return [pscustomobject]@{ released = $true; reason = 'released'; pending_tree = $pending } } return [pscustomobject]@{ released = $false; reason = 'not-owner'; pending_tree = $null } } # Stamp the lease's owner PROCESS (pid + start identity) after the reviewer supervisor is spawned, so crash # recovery tracks the process actually running the review. Owner-token + generation guarded. function Update-ContinuousCoReviewLineageLeaseOwnerProcess { param( [Parameter(Mandatory)][string]$RepoRoot, [Parameter(Mandatory)][AllowEmptyString()][string]$LineageId, [Parameter(Mandatory)][AllowEmptyString()][string]$Generation, [Parameter(Mandatory)][AllowEmptyString()][string]$OwnerToken, [Parameter(Mandatory)][int]$OwnerPid ) $existing = Get-ContinuousCoReviewLineageLease -RepoRoot $RepoRoot -LineageId $LineageId if ($null -eq $existing) { return $false } if (([string](Get-ContinuousCoReviewLeaseProp -Object $existing -Name 'owner_token') -cne $OwnerToken) -or ([string](Get-ContinuousCoReviewLeaseProp -Object $existing -Name 'generation') -cne $Generation)) { return $false } try { $existing | Add-Member -NotePropertyName 'pid' -NotePropertyValue $OwnerPid -Force $existing | Add-Member -NotePropertyName 'process_start_id' -NotePropertyValue (Get-ContinuousCoReviewProcessStartIdentity -ProcessId $OwnerPid) -Force ($existing | ConvertTo-Json -Depth 6) | Set-Content -LiteralPath (Get-ContinuousCoReviewLineageLeasePath -RepoRoot $RepoRoot -LineageId $LineageId) -Encoding UTF8 return $true } catch { return $false } } # VERDICT AUTHORITY is NOT lease ownership alone (maintainer 2026-07-13). Promotion/blocking authority requires # ALL of: owner match (run_id + owner token); generation == the RESULT's reviewed-tree digest; the T019 identity # joins pass (reviewed tree + baseline); and the result is NOT superseded by the current digest. # SUPERVISOR SELF-ADOPTION at startup (lease-lifecycle hardening 2026-07-15): closes the # parent-crash-before-handoff window. The spawning parent stamps the lease's owner process to the supervisor # AFTER the spawn - if the parent dies in between, the lease names a dead parent and a concurrent fire could # reclaim it and spawn a SECOND reviewer while this supervisor lives. The supervisor therefore adopts the # lease itself at startup (token+generation-matched - the same owner-only rule as every lease mutation, so a # reclaimed/replaced lease REFUSES the adoption and the supervisor must exit rather than run unprotected). # Idempotent with the parent's handoff: both stamp the same supervisor PID. function Confirm-ContinuousCoReviewSupervisorLeaseAdoption { param( [Parameter(Mandatory)][string]$RepoRoot, [Parameter(Mandatory)][AllowEmptyString()][string]$LineageId, [Parameter(Mandatory)][AllowEmptyString()][string]$Generation, [Parameter(Mandatory)][AllowEmptyString()][string]$OwnerToken, [Parameter(Mandatory)][int]$SupervisorPid ) if ([string]::IsNullOrWhiteSpace($LineageId) -or [string]::IsNullOrWhiteSpace($Generation) -or [string]::IsNullOrWhiteSpace($OwnerToken)) { return [pscustomobject]@{ adopted = $false; reason = 'missing-lease-identity (legacy registry without lease fields - adoption not applicable)' ; applicable = $false } } $ok = Update-ContinuousCoReviewLineageLeaseOwnerProcess -RepoRoot $RepoRoot -LineageId $LineageId -Generation $Generation -OwnerToken $OwnerToken -OwnerPid $SupervisorPid if ($ok) { return [pscustomobject]@{ adopted = $true; reason = 'adopted'; applicable = $true } } return [pscustomobject]@{ adopted = $false; reason = 'lease-lost-at-startup: the lease no longer matches this run''s owner token/generation (reclaimed or replaced while unowned) - the supervisor must NOT run unprotected'; applicable = $true } } # THE DETACHED ENTRY'S LEASE GATE (lease-lifecycle hardening 2026-07-15): the one testable boundary the # detached supervisor entry calls FIRST. Reads the run registry's lease identity and self-adopts. Returns # { proceed; reason }: proceed=$false means the supervisor must mark its registry failed and EXIT without # reviewing (its lease was lost - another authoritative reviewer may already own the lineage). A legacy # registry without lease fields proceeds (nothing to adopt - the pre-lease compatibility path). function Invoke-ContinuousCoReviewSupervisorLeaseGate { param( [Parameter(Mandatory)][string]$RepoRoot, [Parameter(Mandatory)][string]$RegistryPath, [Parameter(Mandatory)][int]$SupervisorPid ) try { if (-not (Test-Path -LiteralPath $RegistryPath -PathType Leaf)) { return [pscustomobject]@{ proceed = $true; reason = 'no-registry (nothing to adopt)' } } $reg = Get-Content -LiteralPath $RegistryPath -Raw -Encoding UTF8 | ConvertFrom-Json $props = @($reg.PSObject.Properties.Name) $lineage = if ($props -contains 'lineage_id') { [string]$reg.lineage_id } else { '' } $generation = if ($props -contains 'generation') { [string]$reg.generation } else { '' } $token = if ($props -contains 'owner_token') { [string]$reg.owner_token } else { '' } $adoption = Confirm-ContinuousCoReviewSupervisorLeaseAdoption -RepoRoot $RepoRoot -LineageId $lineage -Generation $generation -OwnerToken $token -SupervisorPid $SupervisorPid if ((-not [bool]$adoption.applicable) -or [bool]$adoption.adopted) { return [pscustomobject]@{ proceed = $true; reason = [string]$adoption.reason } } return [pscustomobject]@{ proceed = $false; reason = [string]$adoption.reason } } catch { # fail toward NOT running an unprotectable reviewer only when we positively know the lease was lost; # an unreadable registry is indistinguishable from legacy - proceed (the orchestrator's own guards run). return [pscustomobject]@{ proceed = $true; reason = ('lease-gate-unreadable: ' + $_.Exception.Message) } } } function Test-ContinuousCoReviewLeasePromotionAuthority { param( [AllowNull()]$Lease, [Parameter(Mandatory)][AllowEmptyString()][string]$CompletingRunId, [AllowEmptyString()][string]$CompletingOwnerToken = '', [Parameter(Mandatory)][AllowEmptyString()][string]$ResultReviewedDigest, [Parameter(Mandatory)][AllowEmptyString()][string]$CurrentDigest, [Parameter(Mandatory)][bool]$IdentityJoinsPass ) $leaseRun = [string](Get-ContinuousCoReviewLeaseProp -Object $Lease -Name 'run_id') $leaseTok = [string](Get-ContinuousCoReviewLeaseProp -Object $Lease -Name 'owner_token') $leaseGen = [string](Get-ContinuousCoReviewLeaseProp -Object $Lease -Name 'generation') # OWNER MATCH requires the run id AND a NON-EMPTY, exactly-equal owner token (review finding f4, run # 20260714T172315119): an empty completing token was a wildcard, so knowledge/forgery of a run ID - or # a legacy/corrupt registry that lost its token - could substitute for lease ownership. A missing token # is now non-authoritative exactly like a wrong one (the completion degrades to advisory downstream); # the live spawn path stamps the acquired lease's owner_token into every registry it writes. $ownerMatch = ($null -ne $Lease) -and (-not [string]::IsNullOrWhiteSpace($CompletingRunId)) -and ($leaseRun -ceq $CompletingRunId) -and (-not [string]::IsNullOrWhiteSpace($CompletingOwnerToken)) -and ($leaseTok -ceq $CompletingOwnerToken) $generationMatchesResult = (-not [string]::IsNullOrWhiteSpace($ResultReviewedDigest)) -and ($leaseGen -ceq $ResultReviewedDigest) $notSuperseded = (-not [string]::IsNullOrWhiteSpace($ResultReviewedDigest)) -and ($ResultReviewedDigest -ceq $CurrentDigest) $authoritative = $ownerMatch -and $generationMatchesResult -and $IdentityJoinsPass -and $notSuperseded $reason = if ($authoritative) { $null } elseif (-not $ownerMatch) { 'not-lease-owner' } elseif (-not $generationMatchesResult) { 'generation-mismatch' } elseif (-not $IdentityJoinsPass) { 'identity-join-failed' } else { 'superseded-by-current' } return [pscustomobject]@{ authoritative = $authoritative owner_match = $ownerMatch generation_matches_result = $generationMatchesResult identity_joins_pass = $IdentityJoinsPass not_superseded = $notSuperseded reason = $reason } } # Resolve the repo's lineage id for the fire path: the STABLE merge-base commit with trunk (resolved to a commit # id so aliases are equivalent - T019 final-correction 3) + the branch target. Falls back to a stable # repo-derived key when git cannot resolve (non-repo / detached / no trunk), so two concurrent fires of the SAME # checkout still contend on the SAME lease file. Uses Get-ContinuousCoReviewLineageId when the contract module is # loaded, else a self-contained hash. function Resolve-ContinuousCoReviewRepoLineageId { param([Parameter(Mandatory)][string]$RepoRoot) $anchor = '' $target = '' try { # Trunk via the ONE shared resolver (replaces this loop's duplicated candidate list). Unlike the gate, # the lineage key must ALWAYS resolve for dedup, so an ambiguous/greenfield/failed trunk is not fatal # here - it simply falls through to the stable HEAD anchor below (two concurrent fires of the same # checkout still contend on the same lease). Guard for a standalone dot-source of only this module. $trunk = '' if (Get-Command -Name 'Resolve-ContinuousCoReviewTrunkRef' -ErrorAction SilentlyContinue) { $resolvedTrunk = Resolve-ContinuousCoReviewTrunkRef -RepoRoot $RepoRoot if ($resolvedTrunk.ok -and -not [string]::IsNullOrWhiteSpace([string]$resolvedTrunk.trunk_ref)) { $trunk = [string]$resolvedTrunk.trunk_ref } } if (-not [string]::IsNullOrWhiteSpace($trunk)) { $mb = (& git -C $RepoRoot merge-base HEAD $trunk 2>$null) if ($LASTEXITCODE -eq 0 -and -not [string]::IsNullOrWhiteSpace($mb)) { $anchor = ([string]$mb).Trim() } } if ([string]::IsNullOrWhiteSpace($anchor)) { $head = (& git -C $RepoRoot rev-parse HEAD 2>$null) if ($LASTEXITCODE -eq 0 -and -not [string]::IsNullOrWhiteSpace($head)) { $anchor = ([string]$head).Trim() } } $br = (& git -C $RepoRoot rev-parse --abbrev-ref HEAD 2>$null) if ($LASTEXITCODE -eq 0 -and -not [string]::IsNullOrWhiteSpace($br)) { $target = ([string]$br).Trim() } } catch { $null = $_ } if ([string]::IsNullOrWhiteSpace($anchor)) { $anchor = 'no-anchor' } if ([string]::IsNullOrWhiteSpace($target)) { $rp = (Resolve-Path -LiteralPath $RepoRoot -ErrorAction SilentlyContinue) $target = if ($null -ne $rp) { $rp.Path } else { [string]$RepoRoot } } if (Get-Command -Name 'Get-ContinuousCoReviewLineageId' -ErrorAction SilentlyContinue) { return Get-ContinuousCoReviewLineageId -AnchorCommitId $anchor -TargetIdentity $target } $bytes = [System.Text.Encoding]::UTF8.GetBytes(("{0}`n{1}" -f $anchor, $target)) return 'lin-' + [System.BitConverter]::ToString([System.Security.Cryptography.SHA256]::HashData($bytes)).Replace('-', '').ToLowerInvariant().Substring(0, 16) } |