在准备环境前提交次全部更改。

This commit is contained in:
Neo
2026-02-19 08:35:13 +08:00
parent ded6dfb9d8
commit 4eac07da47
1387 changed files with 6107191 additions and 33002 deletions

View File

@@ -1,128 +0,0 @@
# audit_flagger.ps1 — 判断 git 工作区是否存在高风险改动
# 兼容 Windows PowerShell 5.1(避免 try{} 内嵌套脚本块的解析器 bug
$ErrorActionPreference = "SilentlyContinue"
function Get-TaipeiNow {
$tz = [TimeZoneInfo]::FindSystemTimeZoneById("Taipei Standard Time")
if ($tz) {
return [TimeZoneInfo]::ConvertTime([DateTimeOffset]::Now, $tz)
}
return [DateTimeOffset]::Now
}
function Sha1Hex([string]$s) {
$sha1 = [System.Security.Cryptography.SHA1]::Create()
$bytes = [System.Text.Encoding]::UTF8.GetBytes($s)
$hash = $sha1.ComputeHash($bytes)
return ([BitConverter]::ToString($hash) -replace "-", "").ToLowerInvariant()
}
function Get-ChangedFiles {
$status = git status --porcelain 2>$null
if ($LASTEXITCODE -ne 0) { return @() }
$result = @()
foreach ($line in $status) {
if ([string]::IsNullOrWhiteSpace($line)) { continue }
$pathPart = $line.Substring([Math]::Min(3, $line.Length - 1)).Trim()
if ($pathPart -match " -> ") { $pathPart = ($pathPart -split " -> ")[-1] }
if ([string]::IsNullOrWhiteSpace($pathPart)) { continue }
$result += $pathPart.Replace("\", "/").Trim()
}
return $result
}
function Test-NoiseFile([string]$f) {
if ($f -match "^docs/audit/") { return $true }
if ($f -match "^\.kiro/\.audit_state\.json$") { return $true }
if ($f -match "^\.kiro/\.last_prompt_id\.json$") { return $true }
if ($f -match "^\.kiro/scripts/") { return $true }
return $false
}
function Write-AuditState([string]$json) {
$null = New-Item -ItemType Directory -Force -Path ".kiro" 2>$null
Set-Content -Path ".kiro/.audit_state.json" -Value $json -Encoding UTF8
}
# --- 主逻辑 ---
# 非 git 仓库直接退出
$null = git rev-parse --is-inside-work-tree 2>$null
if ($LASTEXITCODE -ne 0) { exit 0 }
$allFiles = Get-ChangedFiles
# 过滤噪声
$files = @()
foreach ($f in $allFiles) {
if (-not (Test-NoiseFile $f)) { $files += $f }
}
$files = $files | Sort-Object -Unique
$now = Get-TaipeiNow
if ($files.Count -eq 0) {
$json = '{"audit_required":false,"db_docs_required":false,"reasons":[],"changed_files":[],"change_fingerprint":"","marked_at":"' + $now.ToString("o") + '","last_reminded_at":null}'
Write-AuditState $json
exit 0
}
# 高风险路径("regex|label" 格式,避免 @{} 哈希表在 PS 5.1 的解析问题)
$riskRules = @(
"^apps/etl/pipelines/feiqiu/(api|cli|config|database|loaders|models|orchestration|scd|tasks|utils|quality)/|etl",
"^apps/backend/app/|backend",
"^packages/shared/|shared",
"^db/|db"
)
$reasons = @()
$auditRequired = $false
$dbDocsRequired = $false
foreach ($f in $files) {
foreach ($rule in $riskRules) {
$idx = $rule.LastIndexOf("|")
$pat = $rule.Substring(0, $idx)
$lbl = $rule.Substring($idx + 1)
if ($f -match $pat) {
$auditRequired = $true
$tag = "dir:" + $lbl
if ($reasons -notcontains $tag) { $reasons += $tag }
}
}
if ($f -notmatch "/") {
$auditRequired = $true
if ($reasons -notcontains "root-file") { $reasons += "root-file" }
}
if ($f -match "^db/" -or $f -match "/migrations/" -or $f -match "\.sql$" -or $f -match "\.prisma$") {
$dbDocsRequired = $true
if ($reasons -notcontains "db-schema-change") { $reasons += "db-schema-change" }
}
}
$fp = Sha1Hex(($files -join "`n"))
# 读取已有状态以保留 last_reminded_at
$lastReminded = $null
if (Test-Path ".kiro/.audit_state.json") {
$raw = Get-Content ".kiro/.audit_state.json" -Raw 2>$null
if ($raw) {
$existing = $raw | ConvertFrom-Json 2>$null
if ($existing -and $existing.change_fingerprint -eq $fp) {
$lastReminded = $existing.last_reminded_at
}
}
}
$stateObj = [ordered]@{
audit_required = [bool]$auditRequired
db_docs_required = [bool]$dbDocsRequired
reasons = $reasons
changed_files = $files | Select-Object -First 50
change_fingerprint = $fp
marked_at = $now.ToString("o")
last_reminded_at = $lastReminded
}
Write-AuditState ($stateObj | ConvertTo-Json -Depth 6)
exit 0

View File

@@ -0,0 +1,165 @@
#!/usr/bin/env python3
"""audit_flagger — 判断 git 工作区是否存在高风险改动,写入 .kiro/.audit_state.json
替代原 PowerShell 版本,避免 Windows PowerShell 5.1 解析器 bug。
"""
import hashlib
import json
import os
import re
import subprocess
import sys
from datetime import datetime, timezone, timedelta
TZ_TAIPEI = timezone(timedelta(hours=8))
RISK_RULES = [
(re.compile(r"^apps/etl/pipelines/feiqiu/(api|cli|config|database|loaders|models|orchestration|scd|tasks|utils|quality)/"), "etl"),
(re.compile(r"^apps/backend/app/"), "backend"),
(re.compile(r"^apps/admin-web/src/"), "admin-web"),
(re.compile(r"^apps/miniprogram/(miniapp|miniprogram)/"), "miniprogram"),
(re.compile(r"^packages/shared/"), "shared"),
(re.compile(r"^db/"), "db"),
]
NOISE_PATTERNS = [
re.compile(r"^docs/audit/"),
re.compile(r"^\.kiro/"), # .kiro 配置变更不触发业务审计
re.compile(r"^tmp/"),
re.compile(r"^\.hypothesis/"),
]
DB_PATTERNS = [
re.compile(r"^db/"),
re.compile(r"/migrations/"),
re.compile(r"\.sql$"),
re.compile(r"\.prisma$"),
]
STATE_PATH = os.path.join(".kiro", ".audit_state.json")
def now_taipei():
return datetime.now(TZ_TAIPEI).isoformat()
def sha1hex(s: str) -> str:
return hashlib.sha1(s.encode("utf-8")).hexdigest()
def get_changed_files() -> list[str]:
"""从 git status --porcelain 提取变更文件路径"""
try:
result = subprocess.run(
["git", "status", "--porcelain"],
capture_output=True, text=True, timeout=10
)
if result.returncode != 0:
return []
except Exception:
return []
files = []
for line in result.stdout.splitlines():
if len(line) < 4:
continue
path = line[3:].strip()
if " -> " in path:
path = path.split(" -> ")[-1]
path = path.strip().strip('"').replace("\\", "/")
if path:
files.append(path)
return files
def is_noise(f: str) -> bool:
return any(p.search(f) for p in NOISE_PATTERNS)
def write_state(state: dict):
os.makedirs(".kiro", exist_ok=True)
with open(STATE_PATH, "w", encoding="utf-8") as fh:
json.dump(state, fh, indent=2, ensure_ascii=False)
def main():
# 非 git 仓库直接退出
try:
r = subprocess.run(
["git", "rev-parse", "--is-inside-work-tree"],
capture_output=True, text=True, timeout=5
)
if r.returncode != 0:
return
except Exception:
return
all_files = get_changed_files()
files = sorted(set(f for f in all_files if not is_noise(f)))
now = now_taipei()
if not files:
write_state({
"audit_required": False,
"db_docs_required": False,
"reasons": [],
"changed_files": [],
"change_fingerprint": "",
"marked_at": now,
"last_reminded_at": None,
})
return
reasons = []
audit_required = False
db_docs_required = False
for f in files:
for pattern, label in RISK_RULES:
if pattern.search(f):
audit_required = True
tag = f"dir:{label}"
if tag not in reasons:
reasons.append(tag)
# 根目录散文件
if "/" not in f:
audit_required = True
if "root-file" not in reasons:
reasons.append("root-file")
# DB 文档触发
if any(p.search(f) for p in DB_PATTERNS):
db_docs_required = True
if "db-schema-change" not in reasons:
reasons.append("db-schema-change")
fp = sha1hex("\n".join(files))
# 保留已有状态的 last_reminded_at
last_reminded = None
if os.path.isfile(STATE_PATH):
try:
with open(STATE_PATH, "r", encoding="utf-8") as fh:
existing = json.load(fh)
if existing.get("change_fingerprint") == fp:
last_reminded = existing.get("last_reminded_at")
except Exception:
pass
write_state({
"audit_required": audit_required,
"db_docs_required": db_docs_required,
"reasons": reasons,
"changed_files": files[:50],
"change_fingerprint": fp,
"marked_at": now,
"last_reminded_at": last_reminded,
})
if __name__ == "__main__":
try:
main()
except Exception:
# 绝不阻塞 prompt 提交
pass

View File

@@ -1,72 +0,0 @@
$ErrorActionPreference = "Stop"
function Get-TaipeiNow {
try {
$tz = [TimeZoneInfo]::FindSystemTimeZoneById("Taipei Standard Time")
return [TimeZoneInfo]::ConvertTime([DateTimeOffset]::Now, $tz)
} catch {
return [DateTimeOffset]::Now
}
}
try {
$statePath = ".kiro/.audit_state.json"
if (-not (Test-Path $statePath)) { exit 0 }
$state = $null
try { $state = Get-Content $statePath -Raw | ConvertFrom-Json } catch { exit 0 }
if (-not $state) { exit 0 }
# If no pending audit, do nothing
if (-not $state.audit_required) { exit 0 }
# If working tree is clean (ignoring logs/state), stop reminding
$null = git rev-parse --is-inside-work-tree 2>$null
if ($LASTEXITCODE -ne 0) { exit 0 }
$status = git status --porcelain
$files = @()
foreach ($line in $status) {
if ([string]::IsNullOrWhiteSpace($line)) { continue }
$pathPart = $line.Substring([Math]::Min(3, $line.Length-1)).Trim()
if ($pathPart -match " -> ") { $pathPart = ($pathPart -split " -> ")[-1] }
$p = $pathPart.Replace("\", "/").Trim()
if ($p -and $p -notmatch "^docs/audit/" -and $p -notmatch "^\.kiro/") { $files += $p }
}
if (($files | Sort-Object -Unique).Count -eq 0) {
$state.audit_required = $false
$state.reasons = @()
$state.changed_files = @()
$state.last_reminded_at = $null
($state | ConvertTo-Json -Depth 6) | Set-Content -Path $statePath -Encoding UTF8
exit 0
}
$now = Get-TaipeiNow
$minInterval = [TimeSpan]::FromMinutes(15)
$last = $null
if ($state.last_reminded_at) {
try { $last = [DateTimeOffset]::Parse($state.last_reminded_at) } catch { $last = $null }
}
$shouldRemind = $true
if ($last) {
$elapsed = $now - $last
if ($elapsed -lt $minInterval) { $shouldRemind = $false }
}
if (-not $shouldRemind) { exit 0 }
# Update last_reminded_at (persist even if user ignores)
$state.last_reminded_at = $now.ToString("o")
($state | ConvertTo-Json -Depth 6) | Set-Content -Path $statePath -Encoding UTF8
$reasons = @()
if ($state.reasons) { $reasons = @($state.reasons) }
$reasonText = if ($reasons.Count -gt 0) { ($reasons -join ", ") } else { "high-risk paths changed" }
[Console]::Error.WriteLine("[AUDIT REMINDER] Pending audit detected ($reasonText). Run /audit (Manual: Run /audit hook) to sync docs & write audit artifacts. (rate limit: 15min)")
exit 1
} catch {
exit 0
}

View File

@@ -0,0 +1,107 @@
#!/usr/bin/env python3
"""audit_reminder — Agent 结束时检查是否有待审计改动15 分钟限频提醒。
替代原 PowerShell 版本,避免 Windows PowerShell 5.1 解析器 bug。
"""
import json
import os
import subprocess
import sys
from datetime import datetime, timezone, timedelta
TZ_TAIPEI = timezone(timedelta(hours=8))
STATE_PATH = os.path.join(".kiro", ".audit_state.json")
MIN_INTERVAL = timedelta(minutes=15)
def now_taipei():
return datetime.now(TZ_TAIPEI)
def load_state():
if not os.path.isfile(STATE_PATH):
return None
try:
with open(STATE_PATH, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
return None
def save_state(state):
os.makedirs(".kiro", exist_ok=True)
with open(STATE_PATH, "w", encoding="utf-8") as f:
json.dump(state, f, indent=2, ensure_ascii=False)
def get_real_changes():
"""获取排除噪声后的变更文件"""
try:
r = subprocess.run(["git", "status", "--porcelain"], capture_output=True, text=True, timeout=10)
if r.returncode != 0:
return []
except Exception:
return []
files = []
for line in r.stdout.splitlines():
if len(line) < 4:
continue
path = line[3:].strip().strip('"').replace("\\", "/")
if " -> " in path:
path = path.split(" -> ")[-1]
# 排除审计产物、.kiro 配置、临时文件
if path and not path.startswith("docs/audit/") and not path.startswith(".kiro/") and not path.startswith("tmp/") and not path.startswith(".hypothesis/"):
files.append(path)
return sorted(set(files))
def main():
state = load_state()
if not state:
sys.exit(0)
if not state.get("audit_required"):
sys.exit(0)
# 工作树干净时清除审计状态
real_files = get_real_changes()
if not real_files:
state["audit_required"] = False
state["reasons"] = []
state["changed_files"] = []
state["last_reminded_at"] = None
save_state(state)
sys.exit(0)
now = now_taipei()
# 15 分钟限频
last_str = state.get("last_reminded_at")
if last_str:
try:
last = datetime.fromisoformat(last_str)
if (now - last) < MIN_INTERVAL:
sys.exit(0)
except Exception:
pass
# 更新提醒时间
state["last_reminded_at"] = now.isoformat()
save_state(state)
reasons = state.get("reasons", [])
reason_text = ", ".join(reasons) if reasons else "high-risk paths changed"
sys.stderr.write(
f"[AUDIT REMINDER] Pending audit detected ({reason_text}). "
f"Run /audit (Manual: Run /audit hook) to sync docs & write audit artifacts. "
f"(rate limit: 15min)\n"
)
sys.exit(1)
if __name__ == "__main__":
try:
main()
except Exception:
sys.exit(0)

View File

@@ -1,61 +0,0 @@
$ErrorActionPreference = "Stop"
function Get-TaipeiNow {
try {
$tz = [TimeZoneInfo]::FindSystemTimeZoneById("Taipei Standard Time")
return [TimeZoneInfo]::ConvertTime([DateTimeOffset]::Now, $tz)
} catch {
return [DateTimeOffset]::Now
}
}
try {
$now = Get-TaipeiNow
$promptId = "P{0}" -f $now.ToString("yyyyMMdd-HHmmss")
$promptRaw = $env:USER_PROMPT
if ($null -eq $promptRaw) { $promptRaw = "" }
# 截断过长的 prompt避免意外记录展开的 #context
if ($promptRaw.Length -gt 20000) {
$promptRaw = $promptRaw.Substring(0, 5000) + "`n[TRUNCATED: prompt too long; possible expanded #context]"
}
$summary = ($promptRaw -replace "\s+", " ").Trim()
if ($summary.Length -gt 120) { $summary = $summary.Substring(0, 120) + "" }
if ([string]::IsNullOrWhiteSpace($summary)) { $summary = "(empty prompt)" }
# CHANGE [2026-02-15] intent: 简化为每次直接写独立文件到 prompt_logs/,不再维护 prompt_log.md 中间文件
$logDir = Join-Path "docs" "audit" "prompt_logs"
$null = New-Item -ItemType Directory -Force -Path $logDir 2>$null
$filename = "prompt_log_{0}.md" -f $now.ToString("yyyyMMdd_HHmmss")
$targetLog = Join-Path $logDir $filename
$timestamp = $now.ToString("yyyy-MM-dd HH:mm:ss zzz")
$entry = @"
- [$promptId] $timestamp
- summary: $summary
- prompt:
``````text
$promptRaw
``````
"@
Set-Content -Path $targetLog -Value $entry -Encoding UTF8
# 保存 last prompt id 供下游 /audit 溯源
$stateDir = ".kiro"
$null = New-Item -ItemType Directory -Force -Path $stateDir 2>$null
$lastPrompt = @{
prompt_id = $promptId
at = $now.ToString("o")
} | ConvertTo-Json -Depth 4
Set-Content -Path (Join-Path $stateDir ".last_prompt_id.json") -Value $lastPrompt -Encoding UTF8
exit 0
} catch {
# 不阻塞 prompt 提交
exit 0
}

View File

@@ -0,0 +1,60 @@
#!/usr/bin/env python3
"""prompt_audit_log — 每次提交 prompt 时生成独立日志文件。
替代原 PowerShell 版本,避免 Windows PowerShell 5.1 解析器 bug。
"""
import json
import os
import sys
from datetime import datetime, timezone, timedelta
TZ_TAIPEI = timezone(timedelta(hours=8))
def main():
now = datetime.now(TZ_TAIPEI)
prompt_id = f"P{now.strftime('%Y%m%d-%H%M%S')}"
prompt_raw = os.environ.get("USER_PROMPT", "")
# 截断过长的 prompt避免展开的 #context 占用过多空间)
if len(prompt_raw) > 20000:
prompt_raw = prompt_raw[:5000] + "\n[TRUNCATED: prompt too long; possible expanded #context]"
summary = " ".join(prompt_raw.split()).strip()
if len(summary) > 120:
summary = summary[:120] + ""
if not summary:
summary = "(empty prompt)"
# 写独立日志文件
log_dir = os.path.join("docs", "audit", "prompt_logs")
os.makedirs(log_dir, exist_ok=True)
filename = f"prompt_log_{now.strftime('%Y%m%d_%H%M%S')}.md"
target = os.path.join(log_dir, filename)
timestamp = now.strftime("%Y-%m-%d %H:%M:%S %z")
entry = f"""- [{prompt_id}] {timestamp}
- summary: {summary}
- prompt:
```text
{prompt_raw}
```
"""
with open(target, "w", encoding="utf-8") as f:
f.write(entry)
# 保存 last prompt id 供 /audit 溯源
os.makedirs(".kiro", exist_ok=True)
last_prompt = {"prompt_id": prompt_id, "at": now.isoformat()}
with open(os.path.join(".kiro", ".last_prompt_id.json"), "w", encoding="utf-8") as f:
json.dump(last_prompt, f, indent=2, ensure_ascii=False)
if __name__ == "__main__":
try:
main()
except Exception:
# 不阻塞 prompt 提交
pass