scripts/internal/sync-boundary-state.ps1

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

$sharedGovernancePath = Join-Path (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)) 'extensions\specrew-speckit\scripts\shared-governance.ps1'
if (-not (Test-Path -LiteralPath $sharedGovernancePath -PathType Leaf)) {
    throw "Missing shared governance helper '$sharedGovernancePath'."
}
. $sharedGovernancePath

function Get-SpecrewSessionStatePaths {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ProjectRoot
    )

    $resolvedProjectRoot = Resolve-ProjectPath -Path $ProjectRoot
    return [pscustomobject]@{
        ProjectRoot = $resolvedProjectRoot
        PromptPath  = Join-Path $resolvedProjectRoot '.specrew\last-start-prompt.md'
        ContextPath = Join-Path $resolvedProjectRoot '.specrew\start-context.json'
        IdentityPath = Join-Path $resolvedProjectRoot '.squad\identity\now.md'
        DecisionsPath = Join-Path $resolvedProjectRoot '.squad\decisions.md'
        FeatureJsonPath = Join-Path $resolvedProjectRoot '.specify\feature.json'
    }
}

function ConvertTo-SpecrewFrontmatterValue {
    param([AllowNull()][object]$Value)

    if ($null -eq $Value) {
        return '(none)'
    }

    $stringValue = [string]$Value
    if ([string]::IsNullOrWhiteSpace($stringValue)) {
        return '(none)'
    }

    if ($stringValue -match '^[A-Za-z0-9_\-./:]+$') {
        return $stringValue
    }

    return '"' + ($stringValue.Replace('"', '\"')) + '"'
}

function ConvertFrom-SpecrewFrontmatter {
    param([AllowNull()][string]$Content)

    $frontmatter = [ordered]@{}
    $body = if ($null -eq $Content) { '' } else { $Content }
    if ([string]::IsNullOrWhiteSpace($Content) -or $Content -notmatch '(?ms)^---\s*\r?\n(.*?)\r?\n---\s*\r?\n?(.*)$') {
        return [pscustomobject]@{
            Frontmatter = $frontmatter
            Body        = $body
        }
    }

    foreach ($line in ($Matches[1] -split '\r?\n')) {
        if ($line -notmatch '^\s*([^:]+):\s*(.*?)\s*$') {
            continue
        }

        $key = $Matches[1].Trim()
        $value = $Matches[2].Trim()
        if ($value.StartsWith('"') -and $value.EndsWith('"') -and $value.Length -ge 2) {
            $value = $value.Substring(1, $value.Length - 2).Replace('\"', '"')
        }

        $frontmatter[$key] = $value
    }

    return [pscustomobject]@{
        Frontmatter = $frontmatter
        Body        = $Matches[2]
    }
}

function New-SpecrewMarkdownContent {
    param(
        [Parameter(Mandatory = $true)]
        [System.Collections.Specialized.OrderedDictionary]$Frontmatter,

        [Parameter(Mandatory = $true)]
        [AllowEmptyString()]
        [string]$Body
    )

    $lines = New-Object System.Collections.Generic.List[string]
    $lines.Add('---') | Out-Null
    foreach ($entry in $Frontmatter.GetEnumerator()) {
        $lines.Add(('{0}: {1}' -f $entry.Key, (ConvertTo-SpecrewFrontmatterValue -Value $entry.Value))) | Out-Null
    }
    $lines.Add('---') | Out-Null
    $lines.Add('') | Out-Null

    $trimmedBody = $Body.Trim()
    if (-not [string]::IsNullOrWhiteSpace($trimmedBody)) {
        $lines.Add($trimmedBody) | Out-Null
        $lines.Add('') | Out-Null
    }

    return ($lines -join [Environment]::NewLine)
}

function Get-SpecrewSessionStateFromFrontmatter {
    param(
        [Parameter(Mandatory = $true)]
        [System.Collections.IDictionary]$Frontmatter
    )

    $featureRef = if ($Frontmatter.Contains('session_state_feature')) { [string]$Frontmatter['session_state_feature'] } else { $null }
    $boundaryType = if ($Frontmatter.Contains('session_state_boundary')) { [string]$Frontmatter['session_state_boundary'] } else { $null }
    if ([string]::IsNullOrWhiteSpace($featureRef) -and [string]::IsNullOrWhiteSpace($boundaryType)) {
        return $null
    }

    return [pscustomobject]@{
        feature_ref      = if ([string]::IsNullOrWhiteSpace($featureRef) -or $featureRef -eq '(none)') { $null } else { $featureRef }
        boundary_type    = if ([string]::IsNullOrWhiteSpace($boundaryType) -or $boundaryType -eq '(none)') { $null } else { $boundaryType }
        iteration_number = if ($Frontmatter.Contains('session_state_iteration') -and $Frontmatter['session_state_iteration'] -ne '(none)') { [string]$Frontmatter['session_state_iteration'] } else { $null }
        task_id          = if ($Frontmatter.Contains('session_state_task') -and $Frontmatter['session_state_task'] -ne '(none)') { [string]$Frontmatter['session_state_task'] } else { $null }
        auth_commit_hash = if ($Frontmatter.Contains('session_state_auth_commit') -and $Frontmatter['session_state_auth_commit'] -ne '(none)') { [string]$Frontmatter['session_state_auth_commit'] } else { $null }
        recorded_at      = if ($Frontmatter.Contains('session_state_recorded_at') -and $Frontmatter['session_state_recorded_at'] -ne '(none)') { [string]$Frontmatter['session_state_recorded_at'] } else { $null }
        feature_path     = if ($Frontmatter.Contains('session_state_feature_path') -and $Frontmatter['session_state_feature_path'] -ne '(none)') { [string]$Frontmatter['session_state_feature_path'] } else { $null }
        active           = if ($Frontmatter.Contains('session_state_active')) { [string]$Frontmatter['session_state_active'] } else { 'true' }
    }
}

function Resolve-SpecrewFeatureRef {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ProjectRoot,

        [AllowNull()]
        [string]$FeatureRef
    )

    if ([string]::IsNullOrWhiteSpace($FeatureRef)) {
        return $null
    }

    $trimmedFeatureRef = $FeatureRef.Trim()
    if ($trimmedFeatureRef -match '^[A-Za-z]:\\' -or $trimmedFeatureRef.StartsWith('\\')) {
        return Split-Path -Leaf $trimmedFeatureRef
    }

    if ($trimmedFeatureRef -match '^specs[\\/]') {
        return Split-Path -Leaf $trimmedFeatureRef
    }

    return $trimmedFeatureRef
}

function Resolve-SpecrewFeatureDirectory {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ProjectRoot,

        [AllowNull()]
        [string]$FeatureRef
    )

    $resolvedFeatureRef = Resolve-SpecrewFeatureRef -ProjectRoot $ProjectRoot -FeatureRef $FeatureRef
    if ([string]::IsNullOrWhiteSpace($resolvedFeatureRef)) {
        return $null
    }

    return Join-Path (Resolve-ProjectPath -Path $ProjectRoot) ('specs\' + $resolvedFeatureRef)
}

function Get-SpecrewFeatureNumber {
    param([AllowNull()][string]$FeatureRef)

    if ([string]::IsNullOrWhiteSpace($FeatureRef)) {
        return $null
    }

    if ($FeatureRef -match '^(?<number>\d{3})[-_]') {
        return $Matches['number']
    }

    return $null
}

function Get-SpecrewBoundaryOrder {
    return @('specify', 'clarify', 'plan', 'tasks', 'review-signoff', 'iteration-closeout', 'feature-closeout')
}

function Resolve-SpecrewBoundaryAuthCommitHash {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ProjectRoot,

        [AllowNull()]
        [string]$AuthCommitHash
    )

    if (-not [string]::IsNullOrWhiteSpace($AuthCommitHash) -and $AuthCommitHash -ne 'HEAD') {
        return $AuthCommitHash.Trim()
    }

    $resolvedHead = @(& git -C $ProjectRoot rev-parse --verify HEAD 2>$null)
    if ($LASTEXITCODE -eq 0 -and $resolvedHead.Count -gt 0) {
        $candidateHead = $resolvedHead[0].ToString().Trim()
        if ($candidateHead -match '^[0-9a-f]{40}$') {
            return $candidateHead
        }
    }

    if ($AuthCommitHash -eq 'HEAD') {
        throw "Failed to resolve literal HEAD to a concrete commit hash."
    }

    return $null
}

function New-SpecrewSessionState {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet('specify', 'clarify', 'plan', 'tasks', 'review-signoff', 'iteration-closeout', 'feature-closeout')]
        [string]$BoundaryType,

        [Parameter(Mandatory = $true)]
        [string]$ProjectRoot,

        [AllowNull()]
        [string]$FeatureRef,

        [AllowNull()]
        [string]$IterationNumber,

        [AllowNull()]
        [string]$TaskId,

        [AllowNull()]
        [string]$AuthCommitHash
    )

    $resolvedFeatureRef = Resolve-SpecrewFeatureRef -ProjectRoot $ProjectRoot -FeatureRef $FeatureRef
    $featurePath = Resolve-SpecrewFeatureDirectory -ProjectRoot $ProjectRoot -FeatureRef $resolvedFeatureRef
    $recordedAt = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
    return [pscustomobject]@{
        boundary_type    = $BoundaryType
        feature_ref      = $resolvedFeatureRef
        feature_number   = Get-SpecrewFeatureNumber -FeatureRef $resolvedFeatureRef
        feature_path     = $featurePath
        iteration_number = if ([string]::IsNullOrWhiteSpace($IterationNumber)) { $null } else { $IterationNumber.Trim() }
        task_id          = if ([string]::IsNullOrWhiteSpace($TaskId)) { $null } else { $TaskId.Trim() }
        auth_commit_hash = if ([string]::IsNullOrWhiteSpace($AuthCommitHash)) { $null } else { $AuthCommitHash.Trim() }
        recorded_at      = $recordedAt
        active           = if ($BoundaryType -eq 'feature-closeout') { 'false' } else { 'true' }
    }
}

function Get-SpecrewPromptBody {
    param([pscustomobject]$SessionState)

    if ($SessionState.active -eq 'false') {
        return @"
# Specrew Session State
 
- No active feature.
- Last feature: $(if ($SessionState.feature_ref) { $SessionState.feature_ref } else { '(none)' })
- Last boundary: $($SessionState.boundary_type)
- Recorded at: $($SessionState.recorded_at)
- Authorization commit: $(if ($SessionState.auth_commit_hash) { $SessionState.auth_commit_hash } else { '(none)' })
"@

    }

    return @"
# Specrew Session State
 
- Active feature: $($SessionState.feature_ref)
- Current boundary: $($SessionState.boundary_type)
- Iteration: $(if ($SessionState.iteration_number) { $SessionState.iteration_number } else { '(none)' })
- Task: $(if ($SessionState.task_id) { $SessionState.task_id } else { '(none)' })
- Recorded at: $($SessionState.recorded_at)
- Authorization commit: $(if ($SessionState.auth_commit_hash) { $SessionState.auth_commit_hash } else { '(none)' })
"@

}

function Get-SpecrewIdentityBody {
    param([pscustomobject]$SessionState)

    if ($SessionState.active -eq 'false') {
        return @"
# What We're Focused On
 
No active feature. Last completed feature: $(if ($SessionState.feature_ref) { $SessionState.feature_ref } else { '(none)' }) at the $($SessionState.boundary_type) boundary ($($SessionState.recorded_at)).
"@

    }

    return @"
# What We're Focused On
 
Feature $($SessionState.feature_ref) is active at the $($SessionState.boundary_type) boundary.
 
- Iteration: $(if ($SessionState.iteration_number) { $SessionState.iteration_number } else { '(none)' })
- Task: $(if ($SessionState.task_id) { $SessionState.task_id } else { '(none)' })
- Recorded at: $($SessionState.recorded_at)
"@

}

function Update-SpecrewMarkdownStateFile {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Path,

        [Parameter(Mandatory = $true)]
        [pscustomobject]$SessionState,

        [Parameter(Mandatory = $true)]
        [string]$DefaultBody,

        [AllowNull()]
        [System.Collections.IDictionary]$AdditionalFrontmatter,

        [AllowNull()]
        [string]$PreferredBody,

        [switch]$UsePreferredBody,

        [AllowNull()]
        [string]$SchemaVersion
    )

    $existingContent = if (Test-Path -LiteralPath $Path -PathType Leaf) {
        Get-Content -LiteralPath $Path -Raw -Encoding UTF8
    }
    else {
        ''
    }

    $parsed = ConvertFrom-SpecrewFrontmatter -Content $existingContent
    $frontmatter = [ordered]@{}
    foreach ($entry in $parsed.Frontmatter.GetEnumerator()) {
        if ($entry.Key -like 'session_state_*') {
            continue
        }

        if ($entry.Key -eq 'updated_at') {
            continue
        }

        $frontmatter[$entry.Key] = $entry.Value
    }

    if ($null -ne $AdditionalFrontmatter) {
        foreach ($entry in $AdditionalFrontmatter.GetEnumerator()) {
            $frontmatter[[string]$entry.Key] = $entry.Value
        }
    }

    if (-not [string]::IsNullOrWhiteSpace($SchemaVersion)) {
        $frontmatter['schema'] = $SchemaVersion
    }

    $frontmatter['updated_at'] = $SessionState.recorded_at
    $frontmatter['session_state_active'] = $SessionState.active
    $frontmatter['session_state_boundary'] = $SessionState.boundary_type
    $frontmatter['session_state_feature'] = if ($SessionState.feature_ref) { $SessionState.feature_ref } else { '(none)' }
    $frontmatter['session_state_feature_path'] = if ($SessionState.feature_path) { $SessionState.feature_path } else { '(none)' }
    $frontmatter['session_state_iteration'] = if ($SessionState.iteration_number) { $SessionState.iteration_number } else { '(none)' }
    $frontmatter['session_state_task'] = if ($SessionState.task_id) { $SessionState.task_id } else { '(none)' }
    $frontmatter['session_state_auth_commit'] = if ($SessionState.auth_commit_hash) { $SessionState.auth_commit_hash } else { '(none)' }
    $frontmatter['session_state_recorded_at'] = $SessionState.recorded_at

    $body = if ($UsePreferredBody) {
        if ([string]::IsNullOrWhiteSpace($PreferredBody)) { $DefaultBody } else { $PreferredBody.Trim() }
    }
    elseif ([string]::IsNullOrWhiteSpace($parsed.Body)) {
        $DefaultBody
    }
    else {
        $parsed.Body.Trim()
    }
    $content = New-SpecrewMarkdownContent -Frontmatter $frontmatter -Body $body
    Write-FileAtomically -Path $Path -Content ($content.TrimEnd() + [Environment]::NewLine)
}

function Write-FileAtomically {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Path,

        [Parameter(Mandatory = $true)]
        [AllowEmptyString()]
        [string]$Content
    )

    $resolvedPath = Resolve-ProjectPath -Path $Path
    $directory = Split-Path -Parent $resolvedPath
    if (-not [string]::IsNullOrWhiteSpace($directory) -and -not (Test-Path -LiteralPath $directory -PathType Container)) {
        $null = New-Item -ItemType Directory -Path $directory -Force
    }

    $tempPath = '{0}.{1}.tmp' -f $resolvedPath, ([guid]::NewGuid().ToString('N'))
    try {
        [System.IO.File]::WriteAllText($tempPath, $Content, [System.Text.UTF8Encoding]::new($false))
        if (-not (Test-Path -LiteralPath $tempPath -PathType Leaf)) {
            throw "Atomic write did not create '$tempPath'."
        }

        Move-Item -LiteralPath $tempPath -Destination $resolvedPath -Force -ErrorAction Stop
    }
    catch {
        Remove-OrphanedAtomicWriteArtifacts -Path $resolvedPath -TempPath $tempPath
        throw "Atomic write to '$resolvedPath' failed: $($_.Exception.Message)"
    }
    finally {
        if (Test-Path -LiteralPath $tempPath -PathType Leaf) {
            Remove-Item -LiteralPath $tempPath -Force -ErrorAction SilentlyContinue
        }
    }
}

function Update-SpecrewStartContext {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Path,

        [Parameter(Mandatory = $true)]
        [pscustomobject]$SessionState
    )

    $context = [ordered]@{}
    if (Test-Path -LiteralPath $Path -PathType Leaf) {
        try {
            $existing = Get-Content -LiteralPath $Path -Raw -Encoding UTF8 | ConvertFrom-Json -AsHashtable -Depth 12
            $schema = Get-SpecrewStateSchemaVersion -State $existing -Path $Path
            # v0/v1 behavior: preserve any unrelated properties before refreshing session_state payload
            foreach ($entry in $existing.GetEnumerator()) {
                $context[$entry.Key] = $entry.Value
            }
        }
        catch {
            if (Test-IsUnsupportedSpecrewSchemaError -ErrorRecord $_) {
                throw
            }
            $context = [ordered]@{}
        }
    }

    $context['schema'] = 'v1'
    $context['feature_path'] = if ($SessionState.feature_path) { $SessionState.feature_path } else { $null }
    $context['generated_at_utc'] = $SessionState.recorded_at
    $context['session_state'] = [ordered]@{
        active           = ($SessionState.active -eq 'true')
        boundary_type    = $SessionState.boundary_type
        feature_ref      = $SessionState.feature_ref
        feature_path     = $SessionState.feature_path
        iteration_number = $SessionState.iteration_number
        task_id          = $SessionState.task_id
        auth_commit_hash = $SessionState.auth_commit_hash
        recorded_at      = $SessionState.recorded_at
    }

    Write-FileAtomically -Path $Path -Content (([pscustomobject]$context | ConvertTo-Json -Depth 12) + [Environment]::NewLine)
}

function Clear-SpecrewActiveFeature {
    param(
        [Parameter(Mandatory = $true)]
        [string]$FeatureJsonPath
    )

    $featureJson = [ordered]@{
        feature_directory = ''
    }

    if (Test-Path -LiteralPath $FeatureJsonPath -PathType Leaf) {
        try {
            $existing = Get-Content -LiteralPath $FeatureJsonPath -Raw -Encoding UTF8 | ConvertFrom-Json -AsHashtable -Depth 10
            $schema = Get-SpecrewStateSchemaVersion -State $existing -Path $FeatureJsonPath
            foreach ($entry in $existing.GetEnumerator()) {
                if ($entry.Key -eq 'feature_directory') {
                    continue
                }

                $featureJson[$entry.Key] = $entry.Value
            }
        }
        catch {
            if (Test-IsUnsupportedSpecrewSchemaError -ErrorRecord $_) {
                throw
            }
        }
    }

    $featureJson['schema'] = 'v1'
    Write-FileAtomically -Path $FeatureJsonPath -Content (([pscustomobject]$featureJson | ConvertTo-Json -Depth 10) + [Environment]::NewLine)
}

function Add-SpecrewBoundarySyncLedgerEntry {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ProjectRoot,

        [Parameter(Mandatory = $true)]
        [pscustomobject]$SessionState
    )

    $lines = @(
        ('- **Boundary Type**: {0}' -f $SessionState.boundary_type)
        ('- **Feature Ref**: {0}' -f $(if ($SessionState.feature_ref) { $SessionState.feature_ref } else { '(none)' }))
        ('- **Iteration Number**: {0}' -f $(if ($SessionState.iteration_number) { $SessionState.iteration_number } else { '(none)' }))
        ('- **Task ID**: {0}' -f $(if ($SessionState.task_id) { $SessionState.task_id } else { '(none)' }))
        ('- **Auth Commit Hash**: {0}' -f $(if ($SessionState.auth_commit_hash) { $SessionState.auth_commit_hash } else { '(none)' }))
        ('- **Recorded At**: {0}' -f $SessionState.recorded_at)
    )

    Add-DecisionsLedgerEntry -ProjectRoot $ProjectRoot -Title ('Boundary sync: {0}' -f $SessionState.boundary_type) -Lines $lines | Out-Null
}

function Add-SpecrewBoundarySyncWarningLedgerEntry {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ProjectRoot,

        [Parameter(Mandatory = $true)]
        [string]$BoundaryType,

        [AllowNull()]
        [pscustomobject]$LatestBoundary,

        [Parameter(Mandatory = $true)]
        [string]$Message
    )

    $lines = @(
        ('- **Boundary Type**: {0}' -f $BoundaryType)
        ('- **Latest Recorded Boundary**: {0}' -f $(if ($null -ne $LatestBoundary -and $LatestBoundary.boundary_type) { $LatestBoundary.boundary_type } else { '(none)' }))
        ('- **Recorded At**: {0}' -f ((Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')))
        ('- **Warning**: {0}' -f $Message)
    )

    Add-DecisionsLedgerEntry -ProjectRoot $ProjectRoot -Title ('Boundary sync warning: {0}' -f $BoundaryType) -Lines $lines | Out-Null
}

function Get-LatestSpecrewBoundarySyncState {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ProjectRoot
    )

    $paths = Get-SpecrewSessionStatePaths -ProjectRoot $ProjectRoot
    if (-not (Test-Path -LiteralPath $paths.DecisionsPath -PathType Leaf)) {
        return $null
    }

    $lines = @(Get-Content -LiteralPath $paths.DecisionsPath -Encoding UTF8)
    $startIndex = -1
    for ($index = $lines.Count - 1; $index -ge 0; $index--) {
        if ($lines[$index] -match '^## .* — Boundary sync: ') {
            $startIndex = $index
            break
        }
    }

    if ($startIndex -lt 0) {
        return $null
    }

    $entryLines = New-Object System.Collections.Generic.List[string]
    for ($index = $startIndex + 1; $index -lt $lines.Count; $index++) {
        if ($lines[$index] -match '^## ') {
            break
        }

        $entryLines.Add($lines[$index]) | Out-Null
    }

    $values = @{}
    foreach ($line in $entryLines) {
        if ($line -match '^- \*\*(.+?)\*\*:\s*(.+?)\s*$') {
            $values[$Matches[1]] = $Matches[2]
        }
    }

    if (-not $values.ContainsKey('Boundary Type')) {
        return $null
    }

    return [pscustomobject]@{
        boundary_type    = [string]$values['Boundary Type']
        feature_ref      = if ($values['Feature Ref'] -and $values['Feature Ref'] -ne '(none)') { [string]$values['Feature Ref'] } else { $null }
        iteration_number = if ($values['Iteration Number'] -and $values['Iteration Number'] -ne '(none)') { [string]$values['Iteration Number'] } else { $null }
        task_id          = if ($values['Task ID'] -and $values['Task ID'] -ne '(none)') { [string]$values['Task ID'] } else { $null }
        auth_commit_hash = if ($values['Auth Commit Hash'] -and $values['Auth Commit Hash'] -ne '(none)') { [string]$values['Auth Commit Hash'] } else { $null }
        recorded_at      = if ($values['Recorded At']) { [string]$values['Recorded At'] } else { $null }
        active           = if ([string]::IsNullOrWhiteSpace([string]$values['Feature Ref']) -or $values['Feature Ref'] -eq '(none)') { 'false' } else { 'true' }
    }
}

function Invoke-SpecrewBoundaryStateSync {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ProjectPath,

        [Parameter(Mandatory = $true)]
        [ValidateSet('specify', 'clarify', 'plan', 'tasks', 'review-signoff', 'iteration-closeout', 'feature-closeout')]
        [string]$BoundaryType,

        [AllowNull()]
        [string]$FeatureRef,

        [AllowNull()]
        [string]$IterationNumber,

        [AllowNull()]
        [string]$TaskId,

        [AllowNull()]
        [string]$AuthCommitHash,

        [AllowNull()]
        [string]$IdentityFocusArea,

        [AllowNull()]
        [string]$IdentityActiveIssues,

        [AllowNull()]
        [string]$IdentityBody
    )

    $paths = Get-SpecrewSessionStatePaths -ProjectRoot $ProjectPath
    $effectiveFeatureRef = $FeatureRef
    if ([string]::IsNullOrWhiteSpace($effectiveFeatureRef) -and (Test-Path -LiteralPath $paths.FeatureJsonPath -PathType Leaf)) {
        try {
            $featureJson = Get-Content -LiteralPath $paths.FeatureJsonPath -Raw -Encoding UTF8 | ConvertFrom-Json -AsHashtable -Depth 12
            $schema = Get-SpecrewStateSchemaVersion -State $featureJson -Path $paths.FeatureJsonPath
            # v0/v1 behavior: feature_directory remains the feature-ref source of truth
            if (-not [string]::IsNullOrWhiteSpace([string]$featureJson['feature_directory'])) {
                $effectiveFeatureRef = Split-Path -Leaf ([string]$featureJson['feature_directory'])
            }
        }
        catch {
            if (Test-IsUnsupportedSpecrewSchemaError -ErrorRecord $_) {
                throw
            }
        }
    }

    $latestBoundary = Get-LatestSpecrewBoundarySyncState -ProjectRoot $paths.ProjectRoot
    $boundaryOrder = @(Get-SpecrewBoundaryOrder)
    $expectedBoundaryType = if ($null -eq $latestBoundary) {
        $boundaryOrder[0]
    }
    else {
        $latestBoundaryIndex = [Array]::IndexOf($boundaryOrder, [string]$latestBoundary.boundary_type)
        if ($latestBoundaryIndex -ge 0 -and $latestBoundaryIndex -lt ($boundaryOrder.Count - 1)) {
            $boundaryOrder[$latestBoundaryIndex + 1]
        }
        else {
            $null
        }
    }

    if (-not [string]::IsNullOrWhiteSpace($expectedBoundaryType) -and $expectedBoundaryType -ne $BoundaryType) {
        Add-SpecrewBoundarySyncWarningLedgerEntry -ProjectRoot $paths.ProjectRoot -BoundaryType $BoundaryType -LatestBoundary $latestBoundary -Message ("Expected next boundary '{0}' but received '{1}'." -f $expectedBoundaryType, $BoundaryType)
    }

    $effectiveAuthCommitHash = Resolve-SpecrewBoundaryAuthCommitHash -ProjectRoot $paths.ProjectRoot -AuthCommitHash $AuthCommitHash
    $sessionState = New-SpecrewSessionState `
        -BoundaryType $BoundaryType `
        -ProjectRoot $paths.ProjectRoot `
        -FeatureRef $effectiveFeatureRef `
        -IterationNumber $IterationNumber `
        -TaskId $TaskId `
        -AuthCommitHash $effectiveAuthCommitHash

    $identityAdditionalFrontmatter = $null
    if (-not [string]::IsNullOrWhiteSpace($IdentityFocusArea) -or -not [string]::IsNullOrWhiteSpace($IdentityActiveIssues)) {
        $identityAdditionalFrontmatter = [ordered]@{}
        if (-not [string]::IsNullOrWhiteSpace($IdentityFocusArea)) {
            $identityAdditionalFrontmatter['focus_area'] = $IdentityFocusArea.Trim()
        }
        if (-not [string]::IsNullOrWhiteSpace($IdentityActiveIssues)) {
            $identityAdditionalFrontmatter['active_issues'] = $IdentityActiveIssues.Trim()
        }
    }

    Update-SpecrewMarkdownStateFile -Path $paths.PromptPath -SessionState $sessionState -DefaultBody (Get-SpecrewPromptBody -SessionState $sessionState)
    Update-SpecrewStartContext -Path $paths.ContextPath -SessionState $sessionState
    Update-SpecrewMarkdownStateFile -Path $paths.IdentityPath -SessionState $sessionState -DefaultBody (Get-SpecrewIdentityBody -SessionState $sessionState) -AdditionalFrontmatter $identityAdditionalFrontmatter -PreferredBody $IdentityBody -UsePreferredBody:(-not [string]::IsNullOrWhiteSpace($IdentityBody)) -SchemaVersion 'v1'

    Add-SpecrewBoundarySyncLedgerEntry -ProjectRoot $paths.ProjectRoot -SessionState $sessionState

    if ($BoundaryType -eq 'feature-closeout') {
        Clear-SpecrewActiveFeature -FeatureJsonPath $paths.FeatureJsonPath
    }

    return [pscustomobject]@{
        success          = $true
        boundary_type    = $sessionState.boundary_type
        feature_ref      = $sessionState.feature_ref
        iteration_number = $sessionState.iteration_number
        task_id          = $sessionState.task_id
        recorded_at      = $sessionState.recorded_at
        prompt_path      = $paths.PromptPath
        context_path     = $paths.ContextPath
        identity_path    = $paths.IdentityPath
        decisions_path   = $paths.DecisionsPath
        auth_commit_hash = $sessionState.auth_commit_hash
    }
}