cursor Init 完善多人协同changelog,以及godot相关基础skill和代码规范
This commit is contained in:
@@ -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"
|
||||
|
||||
# === 豁免检查 4:changelog 文件不存在 ===
|
||||
if (-not (Test-Path $changelog)) {
|
||||
Write-Output '{}'
|
||||
exit 0
|
||||
}
|
||||
|
||||
$clMtime = (Get-Item $changelog).LastWriteTime
|
||||
|
||||
# === 豁免检查 5:ack 标记文件足够新 ===
|
||||
$ackFile = ".cursor\changelog\.changelog-ack"
|
||||
if ((Test-Path $ackFile) -and (Get-Item $ackFile).LastWriteTime -ge $clMtime) {
|
||||
Write-Output '{}'
|
||||
exit 0
|
||||
}
|
||||
|
||||
# === 豁免检查 6:stdin 中无文件编辑证据 ===
|
||||
# 防止跨会话残留 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
|
||||
|
||||
Reference in New Issue
Block a user