cursor Init 完善多人协同changelog,以及godot相关基础skill和代码规范

This commit is contained in:
Nostars Developer
2026-07-15 14:38:08 +08:00
parent fc8557f2a9
commit 6446261c69
56 changed files with 3523 additions and 1584 deletions

View File

@@ -1,105 +1,124 @@
# Changelog sync guard — 统一 stop hook
#
# 用确定性逻辑检查:
# 1. 源文件是否比 changelog 更新mtime 比较)
# 2. stdin 中的 agent 上下文是否显示有源文件编辑操作
#
# 静默条件(不触发提醒):
# 1. 环境变量 CURSOR_SKIP_CHANGELOG 被设置sessionStart 在非 agent 模式设置)
# 2. stdin JSON 中 composer_mode 不是 "agent"(如 debug/ask/edit 模式)
# 3. changelog 文件不存在
# 4. .changelog-ack 标记文件存在且足够新(本会话已确认过 changelog 状态)
# 5. 没有源文件比 changelog 更新
# 6. stdin 上下文中没有文件编辑操作的证据(防止跨会话残留 mtime 误触发)
# 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
$input = [Console]::In.ReadToEnd()
function Write-EmptyHookResult {
[Console]::Out.WriteLine('{}')
}
# === 豁免检查 1环境变量跳过标志 ===
$null = [Console]::In.ReadToEnd()
# sessionStart sets this variable outside agent mode.
if ($env:CURSOR_SKIP_CHANGELOG) {
Write-Output '{}'
Write-EmptyHookResult
exit 0
}
# === 豁免检查 2从 stdin 解析 composer_mode ===
try {
$data = $input | ConvertFrom-Json
$mode = $data.composer_mode
if ($mode -and $mode -ne "agent") {
Write-Output '{}'
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
}
} catch {
# JSON 解析失败,继续后续检查
}
# === 豁免检查 3从 stdin 文本匹配 debug 上下文关键词 ===
# stop hook 的 $ARGUMENTS 可能包含对话/工具上下文,检测 debug 相关信号
if ($input -match '"mode"\s*:\s*"debug"' -or
$input -match 'debug[\s_-]?mode' -or
$input -match 'Debug Mode') {
Write-Output '{}'
exit 0
}
$changelog = ".cursor\changelog\changelog-headlines.md"
$srcDir = "art-agent"
# === 豁免检查 4changelog 文件不存在 ===
if (-not (Test-Path $changelog)) {
Write-Output '{}'
exit 0
}
$clMtime = (Get-Item $changelog).LastWriteTime
# === 豁免检查 5ack 标记文件足够新 ===
$ackFile = ".cursor\changelog\.changelog-ack"
if ((Test-Path $ackFile) -and (Get-Item $ackFile).LastWriteTime -ge $clMtime) {
Write-Output '{}'
exit 0
}
# === 豁免检查 6stdin 中无文件编辑证据 ===
# 防止跨会话残留 mtime 差异导致误触发:如果 ack 文件不存在(或过旧),
# 但 stdin 上下文中也没有任何文件写入/编辑操作的痕迹,说明本次会话
# 没有进行代码改动,不应触发提醒。
$hasEditEvidence = (
$input -match 'StrReplace|Write\s*tool|edit_file|file_write|write_to_file' -or
$input -match 'Created file|Modified file|Wrote contents' -or
$input -match '"tool"\s*:\s*"(str_replace|write|edit)"'
$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'
)
if (-not $hasEditEvidence) {
Write-Output '{}'
exit 0
}
# === 核心检查:是否有源文件比 changelog 更新 ===
$extensions = @("*.py", "*.tsx", "*.ts", "*.css")
$excludeDirs = @("node_modules", ".next", "__pycache__", "venv")
$newerFile = $null
foreach ($ext in $extensions) {
$files = Get-ChildItem -Path $srcDir -Filter $ext -Recurse -ErrorAction SilentlyContinue |
Where-Object {
$skip = $false
foreach ($ex in $excludeDirs) {
if ($_.FullName -like "*\$ex\*") { $skip = $true; break }
}
-not $skip -and $_.LastWriteTime -gt $clMtime
} |
Select-Object -First 1
$rootItem = Get-Item -LiteralPath $repoRoot
$directories = New-Object 'System.Collections.Generic.Stack[System.IO.DirectoryInfo]'
$directories.Push($rootItem)
if ($files) {
$newerFile = $files.Name
break
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) {
$msg = "[Hook] Source file updated (e.g. $newerFile) but changelog not synced. Run dev-changelog Skill operation A NOW to write all three changelog layers."
$json = '{"followup_message":"' + $msg.Replace('"','\"') + '"}'
Write-Output $json
$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-Output '{}'
Write-EmptyHookResult
}
exit 0