# Changelog sync guard: compare Godot source/config mtimes against 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' if (-not (Test-Path -LiteralPath $changelog -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 } $comparisonMtime = $changelogMtime if ((Test-Path -LiteralPath $sessionMarker -PathType Leaf)) { $sessionMtime = (Get-Item -LiteralPath $sessionMarker).LastWriteTimeUtc if ($sessionMtime -gt $comparisonMtime) { $comparisonMtime = $sessionMtime } } $includedNames = @('project.godot') $includedExtensions = @( '.gd', '.tscn', '.tres', '.res', '.gdshader', '.cfg', '.json', '.cs', '.csproj', '.py', '.ps1', '.sh' ) $excludedDirectoryNames = @( '.git', '.cursor', '.godot', 'node_modules', '__pycache__', 'venv', '.venv', 'dist', 'build', 'out', 'bin', 'obj', 'export', 'exports', 'coverage', 'tmp', 'temp' ) $newerFile = $null $rootItem = Get-Item -LiteralPath $repoRoot $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) { $newerFile = $entry break } } } if ($newerFile) { $rootPrefix = $repoRoot.TrimEnd('\', '/') + [System.IO.Path]::DirectorySeparatorChar $relativePath = if ($newerFile.FullName.StartsWith($rootPrefix, [System.StringComparison]::OrdinalIgnoreCase)) { $newerFile.FullName.Substring($rootPrefix.Length) } else { $newerFile.Name } $message = "[Hook] Godot source/config is newer than the changelog (e.g. $relativePath). 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