# Changelog sync guard: require Git changes newer than this session and changelog. $utf8 = New-Object System.Text.UTF8Encoding($false) [Console]::InputEncoding = $utf8 [Console]::OutputEncoding = $utf8 function Write-EmptyHookResult { [Console]::Out.WriteLine('{}') } $null = [Console]::In.ReadToEnd() # sessionStart sets this variable outside agent mode. if ($env:CURSOR_SKIP_CHANGELOG) { Write-EmptyHookResult exit 0 } $cursorDir = Split-Path -Parent $PSScriptRoot $repoRoot = Split-Path -Parent $cursorDir $changelog = Join-Path $cursorDir 'changelog\changelog-headlines.md' $ackFile = Join-Path $cursorDir 'changelog\.changelog-ack' $sessionMarker = Join-Path $cursorDir 'changelog\.session-start' $scanConfigPath = Join-Path $PSScriptRoot 'changelog-scan.json' if (-not (Test-Path -LiteralPath $changelog -PathType Leaf) -or -not (Test-Path -LiteralPath $scanConfigPath -PathType Leaf)) { Write-EmptyHookResult exit 0 } if (-not (Test-Path -LiteralPath $sessionMarker -PathType Leaf)) { Write-EmptyHookResult exit 0 } $changelogMtime = (Get-Item -LiteralPath $changelog).LastWriteTimeUtc if ((Test-Path -LiteralPath $ackFile -PathType Leaf) -and (Get-Item -LiteralPath $ackFile).LastWriteTimeUtc -ge $changelogMtime) { Write-EmptyHookResult exit 0 } $gitStatus = @(& git -C $repoRoot status --porcelain=v1 --untracked-files=all -- 2>$null) if ($LASTEXITCODE -ne 0 -or $gitStatus.Count -eq 0) { Write-EmptyHookResult exit 0 } try { $scanConfig = Get-Content -LiteralPath $scanConfigPath -Raw -Encoding UTF8 | ConvertFrom-Json if ($scanConfig.schema_version -ne 1) { throw "Unsupported changelog scan schema." } $includedNames = @( $scanConfig.included_names | Where-Object { $_ -is [string] -and -not [string]::IsNullOrWhiteSpace($_) } ) $includedExtensions = @( $scanConfig.included_extensions | Where-Object { $_ -is [string] -and -not [string]::IsNullOrWhiteSpace($_) } ) $excludedDirectoryNames = @( $scanConfig.excluded_directories | Where-Object { $_ -is [string] -and -not [string]::IsNullOrWhiteSpace($_) } ) } catch { Write-EmptyHookResult exit 0 } if ($includedNames.Count -eq 0 -and $includedExtensions.Count -eq 0) { Write-EmptyHookResult exit 0 } $comparisonMtime = $changelogMtime $sessionMtime = (Get-Item -LiteralPath $sessionMarker).LastWriteTimeUtc if ($sessionMtime -gt $comparisonMtime) { $comparisonMtime = $sessionMtime } $newerFile = $null $newerRelativePath = $null $rootItem = Get-Item -LiteralPath $repoRoot $rootPrefix = $repoRoot.TrimEnd('\', '/') + [System.IO.Path]::DirectorySeparatorChar $directories = New-Object 'System.Collections.Generic.Stack[System.IO.DirectoryInfo]' $directories.Push($rootItem) while ($directories.Count -gt 0 -and -not $newerFile) { $directory = $directories.Pop() try { $entries = Get-ChildItem -LiteralPath $directory.FullName -Force -ErrorAction Stop } catch { continue } foreach ($entry in $entries) { if ($entry.PSIsContainer) { $isReparsePoint = ($entry.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0 if (-not $isReparsePoint -and $excludedDirectoryNames -notcontains $entry.Name) { $directories.Push($entry) } continue } $isIncluded = ($includedNames -contains $entry.Name) -or ($includedExtensions -contains $entry.Extension) if ($isIncluded -and $entry.LastWriteTimeUtc -gt $comparisonMtime) { $relativePath = if ($entry.FullName.StartsWith( $rootPrefix, [System.StringComparison]::OrdinalIgnoreCase )) { $entry.FullName.Substring($rootPrefix.Length) } else { $entry.Name } $gitPath = $relativePath -replace '\\', '/' $pathStatus = @( & git -C $repoRoot status --porcelain=v1 --untracked-files=all -- $gitPath 2>$null ) if ($LASTEXITCODE -eq 0 -and $pathStatus.Count -gt 0) { $newerFile = $entry $newerRelativePath = $relativePath break } } } } if ($newerFile) { $message = "[Hook] Project source/config has an uncommitted change newer than this session and the changelog (e.g. $newerRelativePath). Run dev-changelog Skill operation A now: add a fragment under .cursor/changelog/entries/ and rebuild the changelog views." $result = [ordered]@{ followup_message = $message } [Console]::Out.WriteLine(($result | ConvertTo-Json -Compress -Depth 3)) } else { Write-EmptyHookResult } exit 0