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

214
.cursor/README.html Normal file
View File

@@ -0,0 +1,214 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>.cursor 协作指南</title>
<style>
:root {
color-scheme: dark;
--bg: #0f1117;
--panel: #171b24;
--panel-2: #1d2330;
--border: #2c3546;
--text: #e7ebf3;
--muted: #a8b0bf;
--accent: #7aa2f7;
--ok: #9ece6a;
--warn: #e0af68;
--code: #0b0e14;
--mono: "Cascadia Code", "JetBrains Mono", Consolas, monospace;
--sans: "Segoe UI", "Microsoft YaHei", sans-serif;
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
color: var(--text);
background: radial-gradient(circle at 80% 0, #18233a 0, transparent 34%), var(--bg);
font: 15px/1.7 var(--sans);
}
.layout {
width: min(1120px, calc(100% - 32px));
margin: 0 auto;
padding: 44px 0 72px;
display: grid;
grid-template-columns: 230px minmax(0, 1fr);
gap: 32px;
}
header {
grid-column: 1 / -1;
padding: 34px 38px;
border: 1px solid var(--border);
border-radius: 18px;
background: linear-gradient(135deg, var(--panel-2), var(--panel));
}
header p { max-width: 780px; margin-bottom: 0; color: var(--muted); }
h1 { margin: 0; font-size: 30px; }
h2 { margin: 0 0 16px; font-size: 21px; }
h3 { margin: 24px 0 8px; font-size: 16px; }
nav {
position: sticky;
top: 24px;
align-self: start;
padding: 18px;
border: 1px solid var(--border);
border-radius: 14px;
background: var(--panel);
}
nav strong {
display: block;
margin-bottom: 10px;
color: var(--muted);
font: 12px var(--mono);
letter-spacing: .12em;
}
nav a {
display: block;
padding: 7px 9px;
border-radius: 8px;
color: var(--muted);
text-decoration: none;
}
nav a:hover { color: var(--text); background: var(--panel-2); }
main { min-width: 0; }
section {
margin-bottom: 22px;
padding: 28px 30px;
border: 1px solid var(--border);
border-radius: 14px;
background: var(--panel);
}
ul { padding-left: 21px; }
li { margin: 6px 0; }
code {
padding: 2px 6px;
border: 1px solid var(--border);
border-radius: 6px;
color: var(--warn);
background: var(--panel-2);
font: 13px var(--mono);
}
pre {
overflow-x: auto;
padding: 15px 17px;
border: 1px solid var(--border);
border-radius: 10px;
background: var(--code);
}
pre code { padding: 0; border: 0; color: #c8d3f5; background: transparent; }
.callout {
margin: 16px 0;
padding: 12px 15px;
border-left: 3px solid var(--accent);
border-radius: 0 9px 9px 0;
color: var(--muted);
background: rgba(122, 162, 247, .08);
}
.callout.ok {
border-left-color: var(--ok);
background: rgba(158, 206, 106, .08);
}
.tag {
display: inline-block;
margin: 14px 7px 0 0;
padding: 4px 9px;
border: 1px solid var(--border);
border-radius: 999px;
color: var(--muted);
font: 12px var(--mono);
}
footer {
grid-column: 1 / -1;
text-align: center;
color: var(--muted);
font-size: 13px;
}
@media (max-width: 800px) {
.layout { grid-template-columns: 1fr; }
nav { position: static; }
header { padding: 28px; }
section { padding: 24px; }
}
</style>
</head>
<body>
<div class="layout">
<header>
<h1><code>.cursor</code> 协作指南</h1>
<p>项目级 Agent 记忆、计划和开发日志的基础设施。所有内容均以 <code>.cursor/</code> 为根路径;初始化后的空模板不包含任何项目历史。</p>
<span class="tag">fragment changelog</span>
<span class="tag">deterministic views</span>
<span class="tag">UTF-8 · LF</span>
</header>
<nav aria-label="目录">
<strong>目录</strong>
<a href="#structure">目录结构</a>
<a href="#changelog">开发日志</a>
<a href="#hooks">Git hooks</a>
<a href="#migration">旧格式导入</a>
<a href="#maintenance">维护约定</a>
</nav>
<main>
<section id="structure">
<h2>目录结构</h2>
<ul>
<li><code>.cursor/changelog/entries/</code>:开发日志 fragment 的唯一数据源。</li>
<li><code>.cursor/changelog/changelog-*.md</code>:由生成器确定性生成的只读视图。</li>
<li><code>.cursor/changelog/tools/</code>:生成、迁移、合并导入和 hooks 安装工具。</li>
<li><code>.cursor/deferred/registry.md</code>:延期决策登记。</li>
<li><code>.cursor/pitfalls/pitfalls.md</code>:踩坑记录。</li>
<li><code>.cursor/distillery/</code>:问题、洞察和黄金规则。</li>
<li><code>.cursor/profile/</code>:项目画像及其变更日志。</li>
<li><code>.cursor/plans/</code>:实施计划。</li>
</ul>
</section>
<section id="changelog">
<h2>开发日志</h2>
<p>每条日志保存为 <code>.cursor/changelog/entries/&lt;author&gt;/&lt;id&gt;.md</code>。以下四个视图由生成器维护,不要手改:</p>
<ul>
<li><code>.cursor/changelog/changelog-full.md</code></li>
<li><code>.cursor/changelog/changelog-recent.md</code></li>
<li><code>.cursor/changelog/changelog-headlines.md</code></li>
<li><code>.cursor/changelog/changelog-by-author.md</code></li>
</ul>
<pre><code>python .cursor/changelog/tools/changelog_build.py
python .cursor/changelog/tools/changelog_build.py --check</code></pre>
<div class="callout">生成器要求 Python 3.10 或更高版本。</div>
</section>
<section id="hooks">
<h2>安装 Git hooks</h2>
<h3>Windows</h3>
<pre><code>powershell -ExecutionPolicy Bypass -File .cursor/changelog/tools/install-git-hooks.ps1</code></pre>
<h3>Linux / macOS</h3>
<pre><code>sh .cursor/changelog/tools/install-git-hooks.sh</code></pre>
<p><code>pre-commit</code> 会重建并暂存四个视图;<code>post-merge</code> 会在合并后重新生成视图。每个克隆只需安装一次。</p>
</section>
<section id="migration">
<h2>导入旧格式日志</h2>
<p>仅在需要把旧的聚合日志转换为 fragment 时使用:</p>
<pre><code>powershell -ExecutionPolicy Bypass -File .cursor/changelog/tools/merge-import.ps1</code></pre>
<pre><code>sh .cursor/changelog/tools/merge-import.sh</code></pre>
<p>也可将分支或 ref 作为参数传入。导入工具只添加尚不存在的 ID随后重建并校验视图。</p>
</section>
<section id="maintenance">
<h2>维护约定</h2>
<ul>
<li>生成视图只由 <code>.cursor/changelog/tools/changelog_build.py</code> 维护。</li>
<li>新记录写入对应模板文件,不在模板中保留其他项目的数据。</li>
<li>所有文本文件使用 UTF-8无 BOM和 LF 换行。</li>
</ul>
<div class="callout ok">本页与 <code>.cursor/README.md</code> 语义一致。</div>
</section>
</main>
<footer>干净基础骨架 · 无项目历史</footer>
</div>
</body>
</html>

68
.cursor/README.md Normal file
View File

@@ -0,0 +1,68 @@
# `.cursor` 协作指南
本目录提供项目级的 Agent 记忆、计划和开发日志基础设施。所有内容均以 `.cursor/` 为根路径;初始化后的空模板不包含任何项目历史。
## 目录
- `.cursor/changelog/entries/`:开发日志 fragment 的唯一数据源。
- `.cursor/changelog/changelog-*.md`:由生成器确定性生成的只读视图。
- `.cursor/changelog/tools/`:生成、迁移、合并导入和 Git hooks 安装工具。
- `.cursor/deferred/registry.md`:延期决策登记。
- `.cursor/pitfalls/pitfalls.md`:踩坑记录。
- `.cursor/distillery/`:问题、洞察和黄金规则。
- `.cursor/profile/`:项目画像及其变更日志。
- `.cursor/plans/`:实施计划。
## 开发日志
每条日志保存为 `.cursor/changelog/entries/<author>/<id>.md`。不要手改四个生成视图:
- `.cursor/changelog/changelog-full.md`
- `.cursor/changelog/changelog-recent.md`
- `.cursor/changelog/changelog-headlines.md`
- `.cursor/changelog/changelog-by-author.md`
创建或修改 fragment 后运行:
```bash
python .cursor/changelog/tools/changelog_build.py
python .cursor/changelog/tools/changelog_build.py --check
```
生成器要求 Python 3.10 或更高版本。
### 安装 Git hooks
Windows
```powershell
powershell -ExecutionPolicy Bypass -File .cursor/changelog/tools/install-git-hooks.ps1
```
Linux / macOS
```bash
sh .cursor/changelog/tools/install-git-hooks.sh
```
`pre-commit` 会重建并暂存四个视图;`post-merge` 会在合并后重新生成视图。每个克隆只需安装一次。
### 导入旧格式日志
仅在需要把旧的聚合日志转换为 fragment 时使用:
```powershell
powershell -ExecutionPolicy Bypass -File .cursor/changelog/tools/merge-import.ps1
```
```bash
sh .cursor/changelog/tools/merge-import.sh
```
也可将分支或 ref 作为参数传入。导入工具只添加尚不存在的 ID随后重建并校验视图。
## 维护约定
- 生成视图只由 `.cursor/changelog/tools/changelog_build.py` 维护。
- 新记录写入对应模板文件,不在模板中保留其他项目的数据。
- 所有文本文件使用 UTF-8无 BOM和 LF 换行。

View File

@@ -0,0 +1,4 @@
# Dev Changelog — By Author
按作者聚合的贡献概览,便于多人协作时区分提交来源。
> 本文件由 `tools/changelog_build.py` 自动生成,请勿手改。

View File

@@ -1,5 +1,6 @@
# Dev Changelog — Full
完整的开发改动记录,按时间倒序排列。作为主动 RAG 的数据源,用户手动唤醒时读取。
> 本文件由 `tools/changelog_build.py` 从 `entries/` 自动生成,请勿手改;新增/修改请编辑 `entries/<author>/<id>.md` 后重跑生成器。
## 记录

View File

@@ -1,3 +1,4 @@
# Dev Changelog — Headlines
最近 ~50 次改动的一句话概要,按时间倒序排列。每次会话自动注入上下文。
> 本文件由 `tools/changelog_build.py` 从 `entries/` 自动生成,请勿手改。

View File

@@ -1,4 +1,5 @@
# Dev Changelog — Recent
# Dev Changelog — Recent
最近 ~10 次改动的摘要记录,按时间倒序排列。
最近 ~20 次改动的摘要记录,按时间倒序排列。
当 Agent 检测到当前任务与近期改动相关时自动读取。
> 本文件由 `tools/changelog_build.py` 从 `entries/` 自动生成,请勿手改。

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,336 @@
#!/usr/bin/env python3
"""changelog_build.py — 从 fragment 源生成三层 changelog 视图。
设计目标(解决多人协作 + git merge 冲突):
- 唯一数据源是 ``.cursor/changelog/entries/<author>/<id>.md`` 一条一文件的 fragment。
- 三层视图full / recent / headlines全部由本脚本**确定性生成**,不再手写。
- 因为排序只依赖 fragment 内的 CL 时间戳,与 git merge 顺序无关,所有人重建后逐字节收敛。
只用标准库,兼容 Python 3.10+,可在任意平台运行。
"""
from __future__ import annotations
import argparse
import re
import sys
from dataclasses import dataclass, field
from pathlib import Path
# ---- 路径约定 --------------------------------------------------------------
CHANGELOG_DIR_NAME = ".cursor/changelog"
ENTRIES_SUBDIR = "entries"
FULL_NAME = "changelog-full.md"
RECENT_NAME = "changelog-recent.md"
HEADLINES_NAME = "changelog-headlines.md"
BY_AUTHOR_NAME = "changelog-by-author.md"
RECENT_WINDOW = 20
HEADLINES_WINDOW = 50
# ---- fragment 解析 ---------------------------------------------------------
ID_RE = re.compile(r"^CL-(\d{8})-(\d{3,4})([a-z]*)(?:-(.+))?$")
FM_FENCE = "---"
SECTION_RE = re.compile(r"^<!--\s*(L1|L2|L3)\s*-->\s*$", re.M)
@dataclass
class Fragment:
fid: str
date: str = ""
author: str = "legacy"
author_name: str = ""
type: str = ""
merge_source: str = ""
status: str = "active"
superseded_by: str = ""
title: str = ""
source_chat: str = ""
tags: list[str] = field(default_factory=list)
affected_files: list[str] = field(default_factory=list)
l3: str = ""
l2: str = ""
l1: str = ""
path: Path | None = None
def sort_key(self) -> tuple[str, str, str, str]:
m = ID_RE.match(self.fid)
if not m:
return (self.fid, "", "", "")
date8, time4, suffix, _author = m.groups()
return (date8, time4.zfill(4), suffix, self.fid)
def date_only(self) -> str:
return self.date.split(" ")[0] if self.date else ""
def _parse_frontmatter(text: str) -> dict[str, object]:
data: dict[str, object] = {}
lines = text.splitlines()
i = 0
while i < len(lines):
line = lines[i]
if not line.strip():
i += 1
continue
m = re.match(r"^([A-Za-z_]+):\s*(.*)$", line)
if not m:
i += 1
continue
key, val = m.group(1), m.group(2).strip()
if val == "":
items: list[str] = []
j = i + 1
while j < len(lines) and re.match(r"^\s+-\s+", lines[j]):
items.append(lines[j].strip()[1:].strip())
j += 1
if items:
data[key] = items
i = j
else:
data[key] = ""
i += 1
elif val.startswith("[") and val.endswith("]"):
inner = val[1:-1].strip()
data[key] = [x.strip() for x in inner.split(",") if x.strip()] if inner else []
i += 1
else:
data[key] = val
i += 1
return data
def parse_fragment(path: Path) -> Fragment:
raw = path.read_text(encoding="utf-8")
if not raw.startswith(FM_FENCE):
raise ValueError(f"fragment 缺少 frontmatter: {path}")
rest = raw[len(FM_FENCE):]
end = rest.find("\n" + FM_FENCE)
if end == -1:
raise ValueError(f"fragment frontmatter 未闭合: {path}")
fm_text = rest[:end]
body = rest[end + len("\n" + FM_FENCE):].lstrip("\n")
fm = _parse_frontmatter(fm_text)
sections: dict[str, str] = {}
parts = SECTION_RE.split(body)
it = iter(parts[1:])
for marker, content in zip(it, it):
sections[marker] = content.strip("\n")
def _s(key: str) -> str:
v = fm.get(key, "")
return v if isinstance(v, str) else ""
def _l(key: str) -> list[str]:
v = fm.get(key, [])
return [str(x) for x in v] if isinstance(v, list) else []
frag = Fragment(
fid=_s("id"),
date=_s("date"),
author=_s("author") or "legacy",
author_name=_s("author_name"),
type=_s("type"),
merge_source=_s("merge_source"),
status=_s("status") or "active",
superseded_by=_s("superseded_by"),
title=_s("title"),
source_chat=_s("source_chat"),
tags=_l("tags"),
affected_files=_l("affected_files"),
l3=sections.get("L3", "").strip(),
l2=sections.get("L2", "").strip(),
l1=sections.get("L1", "").strip(),
path=path,
)
if not frag.fid:
raise ValueError(f"fragment 缺少 id: {path}")
return frag
def load_fragments(entries_dir: Path) -> list[Fragment]:
frags: list[Fragment] = []
seen: dict[str, Path] = {}
for path in sorted(entries_dir.rglob("*.md")):
frag = parse_fragment(path)
if frag.fid in seen:
raise ValueError(
f"重复的 CL id {frag.fid}: {path}{seen[frag.fid]}"
)
seen[frag.fid] = path
frags.append(frag)
frags.sort(key=lambda f: f.sort_key(), reverse=True)
return frags
# ---- 渲染 ------------------------------------------------------------------
def _author_tag(frag: Fragment) -> str:
return "" if frag.author in ("", "legacy") else f"@{frag.author} "
def render_headlines(frags: list[Fragment]) -> str:
head = (
"# Dev Changelog — Headlines\n\n"
"最近 ~50 次改动的一句话概要,按时间倒序排列。每次会话自动注入上下文。\n"
"> 本文件由 `tools/changelog_build.py` 从 `entries/` 自动生成,请勿手改。"
)
lines = []
for frag in frags[:HEADLINES_WINDOW]:
merge = f" [merge:{frag.merge_source}]" if frag.merge_source else ""
text = frag.l3 or frag.title
lines.append(f"- [{frag.fid}] {_author_tag(frag)}{text}{merge}")
body = "\n\n".join(lines)
return head + (f"\n\n{body}" if body else "") + "\n"
def render_recent(frags: list[Fragment]) -> str:
head = (
"# Dev Changelog — Recent\n\n"
"最近 ~20 次改动的摘要记录,按时间倒序排列。\n"
"当 Agent 检测到当前任务与近期改动相关时自动读取。\n"
"> 本文件由 `tools/changelog_build.py` 从 `entries/` 自动生成,请勿手改。"
)
blocks = []
for frag in frags[:RECENT_WINDOW]:
b = [f"### [{frag.fid}] {frag.date_only()}{frag.title}"]
if frag.merge_source:
b.append(f"- **merge_source**: {frag.merge_source}")
if frag.author and frag.author != "legacy":
b.append(f"- **author**: {frag.author}")
if frag.tags:
b.append(f"- **tags**: {', '.join(frag.tags)}")
if frag.affected_files:
b.append(f"- **affected_files**: {''.join(frag.affected_files)}")
if frag.l2:
b.append(f"- **summary**: {frag.l2}")
if frag.source_chat:
b.append(f"- **source_chat**: {frag.source_chat}")
blocks.append("\n".join(b))
body = "\n\n".join(blocks)
return head + (f"\n\n{body}" if body else "") + "\n"
def render_full(frags: list[Fragment]) -> str:
head = (
"# Dev Changelog — Full\n\n"
"完整的开发改动记录,按时间倒序排列。作为主动 RAG 的数据源,用户手动唤醒时读取。\n"
"> 本文件由 `tools/changelog_build.py` 从 `entries/` 自动生成,请勿手改;"
"新增/修改请编辑 `entries/<author>/<id>.md` 后重跑生成器。\n\n"
"## 记录"
)
blocks = []
for frag in frags:
b = [f"### [{frag.fid}] {frag.date}{frag.title}"]
if frag.merge_source:
b.append(f"- **merge_source**: {frag.merge_source}")
author_line = frag.author
if frag.author_name:
author_line = f"{frag.author} ({frag.author_name})"
if frag.author and frag.author != "legacy":
b.append(f"- **author**: {author_line}")
if frag.type:
b.append(f"- **type**: {frag.type}")
if frag.status and frag.status != "active":
sup = f"{frag.superseded_by}" if frag.superseded_by else ""
b.append(f"- **status**: {frag.status}{sup}")
if frag.tags:
b.append(f"- **tags**: {', '.join(frag.tags)}")
if frag.affected_files:
af = "\n".join(f" - {x}" for x in frag.affected_files)
b.append(f"- **affected_files**:\n{af}")
if frag.l1:
b.append(frag.l1)
if frag.source_chat:
b.append(f"- **source_chat**: {frag.source_chat}")
blocks.append("\n".join(b))
body = "\n\n".join(blocks)
return head + (f"\n\n{body}" if body else "") + "\n"
def render_by_author(frags: list[Fragment]) -> str:
head = (
"# Dev Changelog — By Author\n\n"
"按作者聚合的贡献概览,便于多人协作时区分提交来源。\n"
"> 本文件由 `tools/changelog_build.py` 自动生成,请勿手改。"
)
by: dict[str, list[Fragment]] = {}
for frag in frags:
by.setdefault(frag.author or "legacy", []).append(frag)
blocks = []
for author in sorted(by):
items = by[author]
name = next((f.author_name for f in items if f.author_name), "")
title = f"## {author}" + (f" ({name})" if name else "") + f"{len(items)}"
rows = [
f"- [{f.fid}] {f.date_only()}{f.title}" for f in items[:HEADLINES_WINDOW]
]
blocks.append(title + "\n" + "\n".join(rows))
body = "\n\n".join(blocks)
return head + (f"\n\n{body}" if body else "") + "\n"
# ---- 入口 ------------------------------------------------------------------
def find_changelog_dir(start: Path) -> Path:
cur = start.resolve()
for cand in [cur, *cur.parents]:
d = cand / CHANGELOG_DIR_NAME
if d.is_dir():
return d
# 脚本自身位于 .cursor/changelog/tools/ 下时回退
here = Path(__file__).resolve().parent.parent
if here.name == "changelog":
return here
raise SystemExit("找不到 .cursor/changelog 目录")
def build(changelog_dir: Path, check: bool = False) -> int:
entries_dir = changelog_dir / ENTRIES_SUBDIR
if not entries_dir.is_dir():
raise SystemExit(f"entries 目录不存在: {entries_dir}")
frags = load_fragments(entries_dir)
targets = {
HEADLINES_NAME: render_headlines(frags),
RECENT_NAME: render_recent(frags),
FULL_NAME: render_full(frags),
BY_AUTHOR_NAME: render_by_author(frags),
}
drift = False
for name, content in targets.items():
path = changelog_dir / name
old = path.read_text(encoding="utf-8") if path.exists() else None
if old != content:
drift = True
if check:
print(f"[drift] {name} 与 fragment 源不一致")
else:
path.write_text(content, encoding="utf-8", newline="\n")
print(f"[write] {name}")
if check:
if drift:
print("视图与 fragment 源不一致,请运行 changelog_build.py 重建。")
return 1
print("视图已与 fragment 源同步。")
return 0
print(f"完成:{len(frags)} 条 fragment → 4 个视图。")
return 0
def main(argv: list[str] | None = None) -> int:
ap = argparse.ArgumentParser(description="从 fragment 源生成三层 changelog 视图")
ap.add_argument("changelog_dir", nargs="?", default=None, help=".cursor/changelog 路径")
ap.add_argument("--check", action="store_true", help="只校验是否漂移不写文件CI 用)")
args = ap.parse_args(argv)
changelog_dir = (
Path(args.changelog_dir) if args.changelog_dir else find_changelog_dir(Path.cwd())
)
return build(changelog_dir, check=args.check)
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,23 @@
#!/bin/sh
# post-merge — git merge / pull 完成后,从 fragment 源确定性重建 changelog 视图,
# 覆盖掉 merge=union 临时产生的乱序/重复内容。
# 若视图被重建(说明 union 留下了脏内容),提示用户提交一次。
ROOT=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0
cd "$ROOT" || exit 0
BUILD=".cursor/changelog/tools/changelog_build.py"
[ -f "$BUILD" ] || exit 0
if command -v python >/dev/null 2>&1; then PY=python
elif command -v python3 >/dev/null 2>&1; then PY=python3
else exit 0
fi
PYTHONIOENCODING=utf-8 "$PY" "$BUILD" >/dev/null 2>&1 || exit 0
if ! git diff --quiet -- .cursor/changelog/changelog-*.md 2>/dev/null; then
echo "[changelog] 已从 fragment 源重建视图merge 后清理)。请 git add + commit 这些视图文件。"
fi
exit 0

View File

@@ -0,0 +1,25 @@
#!/bin/sh
# pre-commit — 从 fragment 源重建 changelog 视图并纳入本次提交。
# 保证每个提交里的 full/recent/headlines/by-author 与 entries/ 严格一致。
# 找不到 python 时静默跳过,不阻塞提交。
ROOT=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0
cd "$ROOT" || exit 0
BUILD=".cursor/changelog/tools/changelog_build.py"
[ -f "$BUILD" ] || exit 0
if command -v python >/dev/null 2>&1; then PY=python
elif command -v python3 >/dev/null 2>&1; then PY=python3
else exit 0
fi
PYTHONIOENCODING=utf-8 "$PY" "$BUILD" >/dev/null 2>&1 || exit 0
git add \
.cursor/changelog/changelog-full.md \
.cursor/changelog/changelog-recent.md \
.cursor/changelog/changelog-headlines.md \
.cursor/changelog/changelog-by-author.md 2>/dev/null || true
exit 0

View File

@@ -0,0 +1,18 @@
# install-git-hooks.ps1 - install changelog pre-commit / post-merge git hooks into this repo.
# Git hooks are not distributed with the repo; run this once per clone (idempotent).
# Usage: powershell -ExecutionPolicy Bypass -File .cursor/changelog/tools/install-git-hooks.ps1
$ErrorActionPreference = "Stop"
$hooksDir = (git rev-parse --git-path hooks 2>$null)
if (-not $hooksDir) { Write-Error "not inside a git repository"; exit 1 }
if (-not (Test-Path $hooksDir)) { New-Item -ItemType Directory -Force -Path $hooksDir | Out-Null }
$src = Join-Path $PSScriptRoot "git-hooks"
foreach ($name in @("pre-commit", "post-merge")) {
$from = Join-Path $src $name
$to = Join-Path $hooksDir $name
Copy-Item -Force $from $to
Write-Output "installed: $to"
}
Write-Output "done. git hooks installed (pre-commit rebuilds + stages views, post-merge rebuilds after merge)."

View File

@@ -0,0 +1,16 @@
#!/bin/sh
# install-git-hooks.sh — 把 changelog 的 pre-commit / post-merge git hook 装进本仓库。
# git hook 不随仓库分发,每个克隆都要跑一次本脚本(幂等)。
# 用法sh .cursor/changelog/tools/install-git-hooks.sh
set -e
HOOKS_DIR=$(git rev-parse --git-path hooks 2>/dev/null) || { echo "不在 git 仓库内"; exit 1; }
mkdir -p "$HOOKS_DIR"
SRC=$(CDPATH= cd -- "$(dirname -- "$0")/git-hooks" && pwd)
for name in pre-commit post-merge; do
cp -f "$SRC/$name" "$HOOKS_DIR/$name"
chmod +x "$HOOKS_DIR/$name"
echo "installed: $HOOKS_DIR/$name"
done
echo "完成。git hook 已安装pre-commit 重建并纳入视图post-merge 合并后重建)。"

View File

@@ -0,0 +1,96 @@
# merge-import.ps1 - harvest a not-yet-fragmented branch's changelog into fragments, then rebuild views.
#
# Use after merging an old-format branch (e.g. master) into your fragment-based branch.
# It captures the other side's old-format changelog entries as fragments (only the IDs
# that don't already exist), rebuilds the four views, and reports any lost IDs.
#
# Usage:
# powershell -ExecutionPolicy Bypass -File .cursor/changelog/tools/merge-import.ps1 # harvest from working-tree (post union-merge)
# powershell -ExecutionPolicy Bypass -File .cursor/changelog/tools/merge-import.ps1 -Ref master # harvest directly from a ref
param([string]$Ref = "")
$ErrorActionPreference = "Stop"
$env:PYTHONIOENCODING = "utf-8"
$root = (git rev-parse --show-toplevel 2>$null)
if (-not $root) { Write-Error "not inside a git repository"; exit 1 }
Set-Location $root
$cl = ".cursor/changelog"
$tools = "$cl/tools"
$pyCmd = (Get-Command python -ErrorAction SilentlyContinue)
if (-not $pyCmd) { $pyCmd = (Get-Command python3 -ErrorAction SilentlyContinue) }
if (-not $pyCmd) { Write-Error "python not found"; exit 1 }
$py = $pyCmd.Source
function Get-Ids($path) {
if (-not (Test-Path $path)) { return @() }
Select-String -Path $path -Pattern "^### \[(CL-[^\]]+)\]" |
ForEach-Object { $_.Matches[0].Groups[1].Value } | Sort-Object -Unique
}
$views = @("changelog-full.md", "changelog-recent.md", "changelog-headlines.md")
$tmp = Join-Path $env:TEMP ("cl-import-" + [guid]::NewGuid().ToString("N"))
New-Item -ItemType Directory -Force -Path $tmp | Out-Null
$enc = New-Object System.Text.UTF8Encoding($false)
try {
if ($Ref) {
Write-Output "harvest source: ref '$Ref'"
foreach ($f in $views) {
$content = (git show "${Ref}:$cl/$f" 2>$null)
if ($content) {
[System.IO.File]::WriteAllText((Join-Path $tmp $f), (($content -join "`n") + "`n"), $enc)
}
}
} else {
Write-Output "harvest source: working-tree (post union-merge)"
foreach ($f in $views) {
if (Test-Path "$cl/$f") { Copy-Item -Force "$cl/$f" (Join-Path $tmp $f) }
}
}
$srcFull = Join-Path $tmp "changelog-full.md"
$srcIds = Get-Ids $srcFull
Write-Output "source entries: $($srcIds.Count)"
# parse source into staging fragments (temp/entries/legacy)
& $py "$tools/migrate_changelog.py" $tmp | Out-Null
# existing fragment ids in the real repo (filename stem == id)
$existing = @()
if (Test-Path "$cl/entries") {
$existing = Get-ChildItem -Recurse "$cl/entries" -Filter *.md | ForEach-Object { $_.BaseName }
}
# copy only NEW ids into real entries/legacy (never overwrite existing fragments)
$stage = Join-Path $tmp "entries/legacy"
$added = 0
if (Test-Path $stage) {
New-Item -ItemType Directory -Force -Path "$cl/entries/legacy" | Out-Null
Get-ChildItem "$stage/*.md" | Where-Object { $existing -notcontains $_.BaseName } | ForEach-Object {
Copy-Item -Force $_.FullName "$cl/entries/legacy/"
$added++
}
}
Write-Output "new fragments imported: $added"
}
finally {
Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
}
# rebuild views from the now-complete entries/
& $py "$tools/changelog_build.py"
# verify: every source id must survive in the rebuilt full view
$nowIds = Get-Ids "$cl/changelog-full.md"
$missing = $srcIds | Where-Object { $nowIds -notcontains $_ }
if ($missing) {
Write-Output "WARNING: source ids missing after rebuild:"
$missing | ForEach-Object { Write-Output " $_" }
exit 2
}
Write-Output "OK: all $($srcIds.Count) source entries present. total now: $($nowIds.Count)."
Write-Output "next: git add -A; git commit"

View File

@@ -0,0 +1,82 @@
#!/bin/sh
# merge-import.sh - harvest a not-yet-fragmented branch's changelog into fragments, then rebuild views.
#
# 合并旧格式分支(如 master后用它把对方旧格式 changelog 条目收割成 fragment
# (只补 entries/ 里还没有的 ID绝不覆盖已有 fragment重建四个视图并报告丢失的 ID。
#
# Usage:
# sh .cursor/changelog/tools/merge-import.sh # harvest from working-tree (post union-merge)
# sh .cursor/changelog/tools/merge-import.sh master # harvest directly from a ref
set -e
REF="${1:-}"
export PYTHONIOENCODING=utf-8
ROOT=$(git rev-parse --show-toplevel 2>/dev/null) || { echo "not inside a git repository"; exit 1; }
cd "$ROOT"
CL=".cursor/changelog"
TOOLS="$CL/tools"
if command -v python >/dev/null 2>&1; then PY=python
elif command -v python3 >/dev/null 2>&1; then PY=python3
else echo "python not found"; exit 1; fi
ids_of() {
[ -f "$1" ] || return 0
grep -oE "^### \[(CL-[^]]+)\]" "$1" | sed -E 's/^### \[(CL-[^]]+)\]/\1/' | sort -u
}
VIEWS="changelog-full.md changelog-recent.md changelog-headlines.md"
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT
if [ -n "$REF" ]; then
echo "harvest source: ref '$REF'"
for f in $VIEWS; do
git show "$REF:$CL/$f" > "$TMP/$f" 2>/dev/null || true
done
else
echo "harvest source: working-tree (post union-merge)"
for f in $VIEWS; do
[ -f "$CL/$f" ] && cp -f "$CL/$f" "$TMP/$f"
done
fi
SRC_IDS=$(ids_of "$TMP/changelog-full.md")
SRC_COUNT=$(printf '%s\n' "$SRC_IDS" | grep -c . || true)
echo "source entries: $SRC_COUNT"
# parse source into staging fragments (temp/entries/legacy)
"$PY" "$TOOLS/migrate_changelog.py" "$TMP" >/dev/null
# existing fragment ids in the real repo (filename stem == id)
mkdir -p "$CL/entries/legacy"
added=0
if [ -d "$TMP/entries/legacy" ]; then
for frag in "$TMP/entries/legacy"/*.md; do
[ -e "$frag" ] || continue
base=$(basename "$frag")
id="${base%.md}"
if ! find "$CL/entries" -name "$base" | grep -q .; then
cp -f "$frag" "$CL/entries/legacy/$base"
added=$((added+1))
fi
done
fi
echo "new fragments imported: $added"
# rebuild views
"$PY" "$TOOLS/changelog_build.py"
# verify
NOW_IDS=$(ids_of "$CL/changelog-full.md")
missing=$(comm -23 <(printf '%s\n' "$SRC_IDS") <(printf '%s\n' "$NOW_IDS") || true)
if [ -n "$missing" ]; then
echo "WARNING: source ids missing after rebuild:"
printf ' %s\n' $missing
exit 2
fi
NOW_COUNT=$(printf '%s\n' "$NOW_IDS" | grep -c . || true)
echo "OK: all $SRC_COUNT source entries present. total now: $NOW_COUNT."
echo "next: git add -A; git commit"

View File

@@ -0,0 +1,252 @@
#!/usr/bin/env python3
"""migrate_changelog.py — 一次性迁移:把旧的三层 changelog 文件拆成 fragment。
把现有 ``changelog-full.md`` 按 ``### [CL-...]`` 头切成一条一文件的 fragment
写到 ``entries/legacy/<id>.md``L3/L2 内容尽量从旧的 headlines/recent 中回收,
回收不到时用标题兜底。迁移后由 ``changelog_build.py`` 重新生成视图。
幂等:重复运行会覆盖 entries/legacy/ 下的同名文件。只用标准库。
"""
from __future__ import annotations
import re
import sys
from pathlib import Path
ENTRY_HEADER_RE = re.compile(r"^### \[(CL-[^\]]+)\]\s*(.*)$")
DATE_TITLE_RE = re.compile(r"^(\d{4}-\d{2}-\d{2}[^—]*?)\s*—\s*(.*)$")
ID_DIGITS_RE = re.compile(r"^CL-(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})")
HEADLINE_LINE_RE = re.compile(r"^- \[(CL-[^\]]+)\]\s*(.*)$")
MERGE_TAG_RE = re.compile(r"\s*\[merge:[^\]]+\]\s*$")
TYPE_BY_TAG = {
"bugfix": "fix",
"fix": "fix",
"feature": "feat",
"feat": "feat",
"docs": "docs",
"refactor": "refactor",
"chore": "chore",
}
def split_entries(full_text: str) -> list[tuple[str, list[str]]]:
"""返回 [(id, [lines...]), ...]lines 含 header 行。"""
lines = full_text.splitlines()
entries: list[tuple[str, list[str]]] = []
cur_id: str | None = None
cur: list[str] = []
for line in lines:
m = ENTRY_HEADER_RE.match(line)
if m:
if cur_id is not None:
entries.append((cur_id, cur))
cur_id = m.group(1)
cur = [line]
elif cur_id is not None:
cur.append(line)
if cur_id is not None:
entries.append((cur_id, cur))
return entries
def parse_headlines(text: str) -> dict[str, str]:
out: dict[str, str] = {}
for line in text.splitlines():
m = HEADLINE_LINE_RE.match(line)
if m:
body = MERGE_TAG_RE.sub("", m.group(2)).strip()
out[m.group(1)] = body
return out
def parse_recent_summaries(text: str) -> dict[str, str]:
out: dict[str, str] = {}
cur_id: str | None = None
for line in text.splitlines():
m = ENTRY_HEADER_RE.match(line)
if m:
cur_id = m.group(1)
continue
if cur_id and line.startswith("- **summary**:"):
out[cur_id] = line[len("- **summary**:"):].strip()
return out
def strip_wrap_backticks(s: str) -> str:
return re.sub(r"^`([^`]+)`$", r"\1", s.strip())
def derive_date(entry_id: str, header_rest: str) -> tuple[str, str]:
"""返回 (date, title)。"""
m = DATE_TITLE_RE.match(header_rest)
if m:
return m.group(1).strip(), m.group(2).strip()
# 无 date —— 标题就是整段date 从 id 推
dm = ID_DIGITS_RE.match(entry_id)
if dm:
y, mo, d, hh, mm = dm.groups()
return f"{y}-{mo}-{d} {hh}:{mm}", header_rest.strip()
return "", header_rest.strip()
def parse_body(lines: list[str]) -> dict[str, object]:
"""解析一个 L1 条目的 body不含 header 行)。"""
tags: list[str] = []
affected: list[str] = []
merge_source = ""
source_chat = ""
l1_lines: list[str] = []
i = 0
n = len(lines)
while i < n:
line = lines[i]
stripped = line.strip()
if stripped.startswith("- **merge_source**:"):
merge_source = stripped[len("- **merge_source**:"):].strip()
i += 1
continue
if stripped.startswith("- **tags**:"):
raw = stripped[len("- **tags**:"):].strip()
tags = [strip_wrap_backticks(t) for t in raw.split(",") if t.strip()]
i += 1
continue
if stripped.startswith("- **affected_files**:"):
inline = stripped[len("- **affected_files**:"):].strip()
if inline:
affected = [strip_wrap_backticks(x) for x in re.split(r"[、,]", inline) if x.strip()]
i += 1
else:
i += 1
while i < n and re.match(r"^\s+-\s+", lines[i]):
item = lines[i].strip()[1:].strip()
affected.append(strip_wrap_backticks(item))
i += 1
continue
if stripped.startswith("- **source_chat**:"):
source_chat = stripped[len("- **source_chat**:"):].strip()
i += 1
continue
l1_lines.append(line)
i += 1
# 去掉首尾空行
while l1_lines and not l1_lines[0].strip():
l1_lines.pop(0)
while l1_lines and not l1_lines[-1].strip():
l1_lines.pop()
return {
"tags": tags,
"affected_files": affected,
"merge_source": merge_source,
"source_chat": source_chat,
"l1": "\n".join(l1_lines),
}
def infer_type(tags: list[str]) -> str:
for t in tags:
if t.lower() in TYPE_BY_TAG:
return TYPE_BY_TAG[t.lower()]
return ""
def fm_scalar(key: str, value: str) -> str:
return f"{key}: {value}".rstrip()
def fm_list(key: str, items: list[str]) -> str:
if not items:
return f"{key}:"
body = "\n".join(f" - {x}" for x in items)
return f"{key}:\n{body}"
def build_fragment_text(
*, fid: str, date: str, author: str, type_: str, merge_source: str,
title: str, source_chat: str, tags: list[str], affected: list[str],
l3: str, l2: str, l1: str,
) -> str:
fm = [
"---",
fm_scalar("id", fid),
fm_scalar("date", date),
fm_scalar("author", author),
fm_scalar("author_name", ""),
fm_scalar("type", type_),
fm_scalar("merge_source", merge_source),
fm_scalar("status", "active"),
fm_scalar("superseded_by", ""),
fm_scalar("title", title),
fm_scalar("source_chat", source_chat),
fm_list("tags", tags),
fm_list("affected_files", affected),
"---",
]
parts = [
"\n".join(fm),
"",
"<!-- L3 -->",
l3,
"",
"<!-- L2 -->",
l2,
"",
"<!-- L1 -->",
l1,
"",
]
return "\n".join(parts)
def main(argv: list[str] | None = None) -> int:
argv = argv if argv is not None else sys.argv[1:]
changelog_dir = Path(argv[0]) if argv else Path(__file__).resolve().parent.parent
author = argv[1] if len(argv) > 1 else "legacy"
full_path = changelog_dir / "changelog-full.md"
recent_path = changelog_dir / "changelog-recent.md"
headlines_path = changelog_dir / "changelog-headlines.md"
if not full_path.exists():
raise SystemExit(f"找不到 {full_path}")
full_text = full_path.read_text(encoding="utf-8")
headlines = parse_headlines(headlines_path.read_text(encoding="utf-8")) if headlines_path.exists() else {}
recent = parse_recent_summaries(recent_path.read_text(encoding="utf-8")) if recent_path.exists() else {}
entries = split_entries(full_text)
out_dir = changelog_dir / "entries" / author
out_dir.mkdir(parents=True, exist_ok=True)
count = 0
for fid, lines in entries:
header_rest = ENTRY_HEADER_RE.match(lines[0]).group(2)
date, title = derive_date(fid, header_rest)
parsed = parse_body(lines[1:])
l3 = headlines.get(fid, "").strip() or title
l2 = recent.get(fid, "").strip() or l3
text = build_fragment_text(
fid=fid,
date=date,
author=author,
type_=infer_type(parsed["tags"]),
merge_source=str(parsed["merge_source"]),
title=title,
source_chat=str(parsed["source_chat"]),
tags=list(parsed["tags"]),
affected=list(parsed["affected_files"]),
l3=l3,
l2=l2,
l1=str(parsed["l1"]),
)
(out_dir / f"{fid}.md").write_text(text, encoding="utf-8", newline="\n")
count += 1
print(f"迁移完成:{count} 条 → {out_dir}")
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -1,11 +1,24 @@
# Deferred Decisions Registry
记录已明确推迟、等待前置条件或需要后续复审的决策。
## Active Items
(暂无延期方案)
<!--
### [decision-id] 决策标题
---
- **status**: deferred
- **tags**:
- **recorded**:
- **updated**:
- **source_chat**:
- **prerequisite**:
- **related_files**:
- **context**:
- **chosen_alternative**:
- **deferred_reason**:
-->
## Completed / Cancelled Items
(暂无已完成或已废弃的方案)
<!-- 完成或取消后将条目移至此处,并更新 status。 -->

View File

@@ -1,5 +1,13 @@
# Golden Rules
经过实践验证(权重 >= 5的精炼认知每次新会话自动注入
经过实践验证、达到采纳阈值的精炼认知。只保留跨任务稳定成立的规则
<!-- 当条目达到权重阈值后由 Agent 自动写入 -->
<!--
### [rule-id] 规则标题
- **规则**:
- **依据**:
- **适用范围**:
- **例外**:
- **权重**:
-->

View File

@@ -1,8 +1,15 @@
# Problem Distillery — Insights
从已解决的顽固问题中蒸馏出的精炼方法论。
从已解决的顽固问题中蒸馏出的可复用方法论。
<!-- 新条目追加在此行下方 -->
<!--
### [insight-id] 洞察标题
- **来源问题**:
- **结论**:
- **适用条件**:
- **权重**:
-->
---
last_distill_date: 2026-04-21
last_distill_date:

View File

@@ -1,5 +1,14 @@
# Problem Distillery — Problems
反复出现的顽固问题追踪记录,按时间倒序排列。
反复出现、尚待稳定解决的顽固问题追踪记录,按时间倒序排列。
<!-- 新条目追加在此行下方 -->
<!--
### [problem-id] 问题标题
- **状态**:
- **症状**:
- **已尝试**:
- **当前假设**:
- **下一步**:
- **关联**:
-->

View File

@@ -1,38 +1,61 @@
# Cursor Hooks 说明
## 结构
## 文件
```
```text
hooks/
├── run-hook.ps1 # Windows dispatcher读取 local-env.json 后分发)
├── session-init.ps1 # sessionStart hookPowerShell
├── session-init.sh # sessionStart hookbash
├── check-changelog.ps1 # stop hookPowerShell
├── check-changelog.sh # stop hookbash
└── README.md # 本文件
├── run-hook.ps1 # Windows dispatcher读取 local-env.json 并校验 JSON 输出
├── session-init.ps1 # sessionStartPowerShell
├── session-init.sh # sessionStartBash
├── check-changelog.ps1 # stop / changelog guardPowerShell
├── check-changelog.sh # stop / changelog guardBash
└── README.md
```
## 跨平台适配
## Windows 配置(当前模板)
`hooks.json` 中的 `command` 字段是**平台绑定的**——Windows 用 `powershell`macOS/Linux 用 `bash`
`.cursor/hooks.json` 统一通过 dispatcher 调用:
### Windows当前
- `sessionStart``powershell -ExecutionPolicy Bypass -File .cursor/hooks/run-hook.ps1 session-init`,超时 5 秒。
- `stop``powershell -ExecutionPolicy Bypass -File .cursor/hooks/run-hook.ps1 check-changelog`,超时 10 秒,`loop_limit` 为 1。
hooks.json 使用 `powershell ... run-hook.ps1` 作为入口dispatcher 根据
`.cursor/local-env.json``shell` 字段决定执行 `.ps1` 还是 `.sh` 脚本。
dispatcher 从 `.cursor/local-env.json` 读取 `shell`
### 迁移到 macOS / Linux
- `powershell` / `pwsh`:运行同名 `.ps1`
- `bash` / `zsh` / `sh`:选择同名 `.sh`,并通过 `bash` 执行。
将 hooks.json 的 command 改为直接调用 `.sh` 脚本:
stdin 以 UTF-8 原样转发。子脚本成功时dispatcher 校验并规范化其 JSON 对象;脚本缺失、配置无效、启动失败、非零退出或输出无效时,只向 stderr 写诊断,并向 hook 返回非阻断的 `{}`
## macOS / Linux 切换
macOS/Linux 不需要 Windows dispatcher。将 `.cursor/hooks.json` 改为 Bash 直调:
```json
{
"version": 1,
"hooks": {
"sessionStart": [{ "command": "bash .cursor/hooks/session-init.sh", "timeout": 5 }],
"stop": [{ "command": "bash .cursor/hooks/check-changelog.sh", "timeout": 10, "loop_limit": 1 }]
"sessionStart": [
{
"command": "bash .cursor/hooks/session-init.sh",
"timeout": 5
}
],
"stop": [
{
"command": "bash .cursor/hooks/check-changelog.sh",
"timeout": 10,
"loop_limit": 1
}
]
}
}
```
同时更新 `.cursor/local-env.json``shell` 字段`bash``zsh`
同时把本机 `.cursor/local-env.json``shell` `bash`(或实际使用的 `zsh`。Bash 脚本不依赖 `jq`;优先用 `python3`Windows Git Bash 可回退 `python`)安全解析/生成 JSON解释器缺失或解析失败时 fail-open 返回 `{}`
## 行为
- `session-init` 清除上一会话的 `.cursor/changelog/.changelog-ack`,并刷新 gitignored 的 `.cursor/changelog/.session-start`。非 `agent` 模式通过 hook `env` 设置 `CURSOR_SKIP_CHANGELOG=1`
- Cursor 的 `stop` 输入不包含可靠的工具编辑历史,因此 `check-changelog` 不解析不存在的“编辑证据”。它只在未 ack、未被 session 环境跳过,且 Godot 源码/配置同时晚于本会话起点与当前 changelog 时返回 `followup_message`,避免旧会话残留 mtime 误报。
- 扫描从仓库根开始,覆盖 `project.godot``.gd``.tscn``.tres``.res``.gdshader``.cfg``.json``.cs``.csproj` 及常用工具脚本;跳过 `.git``.cursor``.godot`、依赖目录、Python 缓存/虚拟环境和常见构建输出目录。
- 所有正常与失败路径都在 stdout 输出一个合法 JSON 对象;检查失败默认不阻断。

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
}
} catch {
# JSON 解析失败,继续后续检查
}
$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'
# === 豁免检查 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 '{}'
if (-not (Test-Path -LiteralPath $changelog -PathType Leaf)) {
Write-EmptyHookResult
exit 0
}
$changelog = ".cursor\changelog\changelog-headlines.md"
$srcDir = "art-agent"
# === 豁免检查 4changelog 文件不存在 ===
if (-not (Test-Path $changelog)) {
Write-Output '{}'
$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
}
$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
$comparisonMtime = $changelogMtime
if ((Test-Path -LiteralPath $sessionMarker -PathType Leaf)) {
$sessionMtime = (Get-Item -LiteralPath $sessionMarker).LastWriteTimeUtc
if ($sessionMtime -gt $comparisonMtime) {
$comparisonMtime = $sessionMtime
}
}
# === 豁免检查 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
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 {
Write-Output '{}'
$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

View File

@@ -1,58 +1,132 @@
#!/usr/bin/env bash
# Changelog sync guard — stop hook (bash 版)
# Changelog sync guard:比较 Godot 源码/配置与本会话起点、changelog 的 mtime。
input=$(cat)
emit_empty() {
printf '{}\n'
}
# === 豁免检查 1环境变量跳过标志 ===
if [ -n "$CURSOR_SKIP_CHANGELOG" ]; then
echo '{}'; exit 0
fi
cat >/dev/null
if [ -n "${CURSOR_SKIP_CHANGELOG:-}" ]; then
emit_empty
exit 0
fi
script_dir=$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)
cursor_dir=$(dirname -- "$script_dir")
repo_root=$(dirname -- "$cursor_dir")
changelog="$cursor_dir/changelog/changelog-headlines.md"
ack_file="$cursor_dir/changelog/.changelog-ack"
session_marker="$cursor_dir/changelog/.session-start"
if [ ! -f "$changelog" ]; then
emit_empty
exit 0
fi
get_mtime() {
stat -c %Y "$1" 2>/dev/null || stat -f %m "$1" 2>/dev/null
}
if [ -f "$ack_file" ]; then
changelog_mtime=$(get_mtime "$changelog")
ack_mtime=$(get_mtime "$ack_file")
if [ -n "$changelog_mtime" ] && [ -n "$ack_mtime" ] &&
[ "$ack_mtime" -ge "$changelog_mtime" ] 2>/dev/null; then
emit_empty
exit 0
fi
fi
comparison_file=$changelog
if [ -f "$session_marker" ]; then
changelog_mtime=$(get_mtime "$changelog")
session_mtime=$(get_mtime "$session_marker")
if [ -n "$changelog_mtime" ] && [ -n "$session_mtime" ] &&
[ "$session_mtime" -gt "$changelog_mtime" ] 2>/dev/null; then
comparison_file=$session_marker
fi
fi
newer_file=''
while IFS= read -r candidate; do
newer_file=$candidate
break
done < <(
find "$repo_root" \
\( -type d \( \
-name '.git' -o \
-name '.cursor' -o \
-name '.godot' -o \
-name 'node_modules' -o \
-name '__pycache__' -o \
-name 'venv' -o \
-name '.venv' -o \
-name 'dist' -o \
-name 'build' -o \
-name 'out' -o \
-name 'bin' -o \
-name 'obj' -o \
-name 'export' -o \
-name 'exports' -o \
-name 'coverage' -o \
-name 'tmp' -o \
-name 'temp' \
\) -prune \) -o \
\( -type f \( \
-iname 'project.godot' -o \
-iname '*.gd' -o \
-iname '*.tscn' -o \
-iname '*.tres' -o \
-iname '*.res' -o \
-iname '*.gdshader' -o \
-iname '*.cfg' -o \
-iname '*.json' -o \
-iname '*.cs' -o \
-iname '*.csproj' -o \
-iname '*.py' -o \
-iname '*.ps1' -o \
-iname '*.sh' \
\) -newer "$comparison_file" -print \) 2>/dev/null
)
if [ -z "$newer_file" ]; then
emit_empty
exit 0
fi
relative_file=${newer_file#"$repo_root"/}
message="[Hook] Godot source/config is newer than the changelog (e.g. $relative_file). Run dev-changelog Skill operation A now: add a fragment under .cursor/changelog/entries/ and rebuild the changelog views."
python_cmd=''
for candidate in python3 python; do
if command -v "$candidate" >/dev/null 2>&1 &&
"$candidate" -c 'import json' >/dev/null 2>&1; then
python_cmd=$candidate
break
fi
done
if [ -n "$python_cmd" ]; then
result=$(
printf '%s' "$message" | "$python_cmd" -c '
import json
import sys
print(json.dumps(
{"followup_message": sys.stdin.read()},
ensure_ascii=False,
separators=(",", ":"),
))
' 2>/dev/null
) || result='{}'
if [ -n "$result" ]; then
printf '%s\n' "$result"
else
emit_empty
fi
else
emit_empty
fi
# === 豁免检查 2从 stdin 解析 composer_mode ===
mode=$(echo "$input" | python3 -c "import sys,json; print(json.load(sys.stdin).get('composer_mode',''))" 2>/dev/null || echo "")
if [ -n "$mode" ] && [ "$mode" != "agent" ]; then
echo '{}'; exit 0
fi
# === 豁免检查 3从 stdin 文本匹配 debug 上下文关键词 ===
if echo "$input" | grep -qiE '"mode"\s*:\s*"debug"|debug[\s_-]?mode|Debug Mode'; then
echo '{}'; exit 0
fi
changelog=".cursor/changelog/changelog-headlines.md"
srcDir="art-agent"
# === 豁免检查 4changelog 文件不存在 ===
if [ ! -f "$changelog" ]; then
echo '{}'; exit 0
fi
clMtime=$(stat -c %Y "$changelog" 2>/dev/null || stat -f %m "$changelog" 2>/dev/null)
# === 豁免检查 5ack 标记文件足够新 ===
ackFile=".cursor/changelog/.changelog-ack"
if [ -f "$ackFile" ]; then
ackMtime=$(stat -c %Y "$ackFile" 2>/dev/null || stat -f %m "$ackFile" 2>/dev/null)
if [ "$ackMtime" -ge "$clMtime" ] 2>/dev/null; then
echo '{}'; exit 0
fi
fi
# === 豁免检查 6stdin 中无文件编辑证据 ===
if ! echo "$input" | grep -qE 'StrReplace|Write\s*tool|edit_file|file_write|write_to_file|Created file|Modified file|Wrote contents|"tool"\s*:\s*"(str_replace|write|edit)"'; then
echo '{}'; exit 0
fi
# === 核心检查:是否有源文件比 changelog 更新 ===
newerFile=$(find "$srcDir" -type f \( -name "*.py" -o -name "*.tsx" -o -name "*.ts" -o -name "*.css" \) \
! -path "*/node_modules/*" ! -path "*/.next/*" ! -path "*/__pycache__/*" ! -path "*/venv/*" \
-newer "$changelog" -print -quit 2>/dev/null)
if [ -n "$newerFile" ]; then
fname=$(basename "$newerFile")
msg="[Hook] Source file updated (e.g. $fname) but changelog not synced. Run dev-changelog Skill operation A NOW to write all three changelog layers."
echo "{\"followup_message\":\"$msg\"}"
else
echo '{}'
fi
exit 0

View File

@@ -1,35 +1,111 @@
# run-hook.ps1 — 通用 hook dispatcher
# 从 .cursor/local-env.json 读取 shell 类型,决定执行 .ps1 还是 .sh 脚本
# 用法powershell -ExecutionPolicy Bypass -File .cursor/hooks/run-hook.ps1 <hook-name>
# 示例run-hook.ps1 session-init → 执行 session-init.ps1 或 session-init.sh
# Hook dispatcher: select the PowerShell or Bash implementation from local-env.json.
param([string]$HookName)
$localEnvPath = ".cursor\local-env.json"
$shell = "powershell"
$utf8 = New-Object System.Text.UTF8Encoding($false)
[Console]::InputEncoding = $utf8
[Console]::OutputEncoding = $utf8
function Write-EmptyHookResult {
[Console]::Out.WriteLine('{}')
}
$inputPayload = [Console]::In.ReadToEnd()
if (Test-Path $localEnvPath) {
try {
$config = Get-Content $localEnvPath -Raw | ConvertFrom-Json
if ($config.shell) { $shell = $config.shell }
} catch {}
if ([string]::IsNullOrWhiteSpace($HookName) -or $HookName -notmatch '^[A-Za-z0-9][A-Za-z0-9._-]*$') {
throw "Invalid hook name."
}
$hookDir = ".cursor\hooks"
$input = [Console]::In.ReadToEnd()
$cursorDir = Split-Path -Parent $PSScriptRoot
$repoRoot = Split-Path -Parent $cursorDir
$localEnvPath = Join-Path $cursorDir 'local-env.json'
$shell = 'powershell'
if ($shell -eq "powershell") {
$scriptPath = Join-Path $hookDir "$HookName.ps1"
if (Test-Path $scriptPath) {
$input | powershell -ExecutionPolicy Bypass -File $scriptPath
} else {
Write-Output '{}'
if (Test-Path -LiteralPath $localEnvPath -PathType Leaf) {
try {
$config = Get-Content -LiteralPath $localEnvPath -Raw -Encoding UTF8 | ConvertFrom-Json
if ($config.shell -is [string] -and -not [string]::IsNullOrWhiteSpace($config.shell)) {
$shell = $config.shell.Trim().ToLowerInvariant()
}
} else {
$scriptPath = Join-Path $hookDir "$HookName.sh"
if (Test-Path $scriptPath) {
$input | bash $scriptPath
} else {
Write-Output '{}'
} catch {
[Console]::Error.WriteLine("[run-hook] Ignoring invalid local-env.json: {0}", $_.Exception.Message)
}
}
$processInfo = New-Object System.Diagnostics.ProcessStartInfo
$processInfo.UseShellExecute = $false
$processInfo.CreateNoWindow = $true
$processInfo.RedirectStandardInput = $true
$processInfo.RedirectStandardOutput = $true
$processInfo.RedirectStandardError = $true
$processInfo.WorkingDirectory = $repoRoot
if ($processInfo.PSObject.Properties.Name -contains 'StandardInputEncoding') {
$processInfo.StandardInputEncoding = $utf8
$processInfo.StandardOutputEncoding = $utf8
$processInfo.StandardErrorEncoding = $utf8
}
switch ($shell) {
{ $_ -in @('powershell', 'powershell.exe', 'pwsh', 'pwsh.exe') } {
$scriptPath = Join-Path $PSScriptRoot "$HookName.ps1"
if (-not (Test-Path -LiteralPath $scriptPath -PathType Leaf)) {
throw "PowerShell hook not found: $HookName"
}
$processInfo.FileName = if ($shell -in @('pwsh', 'pwsh.exe')) { 'pwsh' } else { 'powershell' }
$processInfo.Arguments = "-NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -File `"$scriptPath`""
break
}
{ $_ -in @('bash', 'bash.exe', 'zsh', 'sh') } {
$scriptPath = (Join-Path $PSScriptRoot "$HookName.sh") -replace '\\', '/'
if (-not (Test-Path -LiteralPath $scriptPath -PathType Leaf)) {
throw "Shell hook not found: $HookName"
}
$processInfo.FileName = 'bash'
$processInfo.Arguments = "`"$scriptPath`""
break
}
default {
throw "Unsupported shell '$shell'."
}
}
$process = New-Object System.Diagnostics.Process
$process.StartInfo = $processInfo
if (-not $process.Start()) {
throw "Unable to start hook process."
}
$stdoutTask = $process.StandardOutput.ReadToEndAsync()
$stderrTask = $process.StandardError.ReadToEndAsync()
$process.StandardInput.Write($inputPayload)
$process.StandardInput.Close()
$process.WaitForExit()
$stdout = $stdoutTask.Result
$stderr = $stderrTask.Result
if (-not [string]::IsNullOrWhiteSpace($stderr)) {
[Console]::Error.WriteLine($stderr.TrimEnd())
}
if ($process.ExitCode -ne 0) {
throw "Hook exited with code $($process.ExitCode)."
}
$trimmed = $stdout.Trim()
if ([string]::IsNullOrWhiteSpace($trimmed) -or -not $trimmed.StartsWith('{')) {
throw "Hook returned an empty or non-object response."
}
$result = $trimmed | ConvertFrom-Json
$normalized = $result | ConvertTo-Json -Compress -Depth 20
if ([string]::IsNullOrWhiteSpace($normalized) -or -not $normalized.StartsWith('{')) {
throw "Hook response is not a JSON object."
}
[Console]::Out.WriteLine($normalized)
} catch {
[Console]::Error.WriteLine("[run-hook] {0}", $_.Exception.Message)
Write-EmptyHookResult
}
exit 0

View File

@@ -1,23 +1,48 @@
# session-init.ps1 — 会话启动时检测 composer_mode非 agent 模式设置跳过标志
#
# sessionStart input 包含 composer_mode 字段("agent" / "ask" / "edit" / "debug" 等)
# 通过 env 输出的环境变量会传递给同会话内所有后续 hook
# sessionStart: reset changelog runtime markers and skip checks outside agent mode.
$utf8 = New-Object System.Text.UTF8Encoding($false)
[Console]::InputEncoding = $utf8
[Console]::OutputEncoding = $utf8
$input = [Console]::In.ReadToEnd()
$inputPayload = [Console]::In.ReadToEnd()
$mode = $null
try {
$data = $input | ConvertFrom-Json
if (-not [string]::IsNullOrWhiteSpace($inputPayload)) {
$data = $inputPayload | ConvertFrom-Json
if ($data.composer_mode -is [string]) {
$mode = $data.composer_mode
}
}
} catch {
$mode = $null
}
Remove-Item ".cursor\changelog\.changelog-ack" -ErrorAction SilentlyContinue
if ($mode -and $mode -ne "agent") {
$json = '{"env":{"CURSOR_SKIP_CHANGELOG":"1","CURSOR_COMPOSER_MODE":"' + $mode + '"}}'
Write-Output $json
} else {
Write-Output '{}'
$cursorDir = Split-Path -Parent $PSScriptRoot
$ackFile = Join-Path $cursorDir 'changelog\.changelog-ack'
$sessionMarker = Join-Path $cursorDir 'changelog\.session-start'
Remove-Item -LiteralPath $ackFile -Force -ErrorAction SilentlyContinue
try {
$stream = [System.IO.File]::Open(
$sessionMarker,
[System.IO.FileMode]::Create,
[System.IO.FileAccess]::Write,
[System.IO.FileShare]::ReadWrite
)
$stream.Dispose()
} catch {
# Runtime marker failure must not block session startup.
}
if (-not [string]::IsNullOrWhiteSpace($mode) -and $mode -ne 'agent') {
$result = [ordered]@{
env = [ordered]@{
CURSOR_SKIP_CHANGELOG = '1'
CURSOR_COMPOSER_MODE = $mode
}
}
[Console]::Out.WriteLine(($result | ConvertTo-Json -Compress -Depth 4))
} else {
[Console]::Out.WriteLine('{}')
}
exit 0

View File

@@ -1,15 +1,53 @@
#!/usr/bin/env bash
# session-init.sh — 会话启动时检测 composer_mode非 agent 模式设置跳过标志
# sessionStart重置 changelog 运行态标记,并在非 agent 模式下跳过检查。
input=$(cat)
script_dir=$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)
cursor_dir=$(dirname -- "$script_dir")
rm -f -- "$cursor_dir/changelog/.changelog-ack" 2>/dev/null
: > "$cursor_dir/changelog/.session-start" 2>/dev/null || true
mode=$(echo "$input" | python3 -c "import sys,json; print(json.load(sys.stdin).get('composer_mode',''))" 2>/dev/null || echo "")
rm -f ".cursor/changelog/.changelog-ack"
if [ -n "$mode" ] && [ "$mode" != "agent" ]; then
echo "{\"env\":{\"CURSOR_SKIP_CHANGELOG\":\"1\",\"CURSOR_COMPOSER_MODE\":\"$mode\"}}"
else
echo "{}"
python_cmd=''
for candidate in python3 python; do
if command -v "$candidate" >/dev/null 2>&1 &&
"$candidate" -c 'import json' >/dev/null 2>&1; then
python_cmd=$candidate
break
fi
done
if [ -n "$python_cmd" ]; then
result=$(
printf '%s' "$input" | "$python_cmd" -c '
import json
import sys
try:
data = json.load(sys.stdin)
mode = data.get("composer_mode", "")
if not isinstance(mode, str):
mode = ""
except Exception:
mode = ""
if mode and mode != "agent":
print(json.dumps({
"env": {
"CURSOR_SKIP_CHANGELOG": "1",
"CURSOR_COMPOSER_MODE": mode,
}
}, ensure_ascii=False, separators=(",", ":")))
else:
print("{}")
' 2>/dev/null
) || result='{}'
if [ -n "$result" ]; then
printf '%s\n' "$result"
else
printf '{}\n'
fi
else
printf '{}\n'
fi
exit 0

View File

@@ -1,5 +1,9 @@
{
"_comment": "设备绑定的本地环境配置。复制为 local-env.json 并修改为本机实际值。",
"nodejs_path": "C:\\Users\\<username>\\AppData\\Local\\nodejs",
"shell": "powershell"
"_comment": "复制为 .cursor/local-env.json 后填写本机值local-env.json 仅供本机使用,不应提交。",
"shell": "powershell",
"_shell_comment": "Windows 可用 powershell 或 pwsh使用 Git Bash 时可填 bash。macOS/Linux 通常填 bash 或 zsh。",
"changelog-author": "<your-changelog-handle>",
"_changelog_author_comment": "用于 changelog 条目的作者标识,例如团队账号或个人 handle。",
"godot4_path": "<path-to-godot-4-executable>",
"_godot4_path_comment": "填写本机 Godot 4 可执行文件路径或可从 PATH 解析的命令名;不要把个人绝对路径写入模板。"
}

View File

@@ -1,6 +1,15 @@
# Pitfall Journal
开发过程中踩过的坑,按时间倒序排列。
Agent 进入 Debug mode 或遇到运行时错误时自动检索匹配。
开发过程中踩过的坑,按时间倒序排列。Agent 进入调试或遇到运行时错误时可检索本文件。
<!--
### [PF-YYYYMMDD-HHMM] 简短标题
- **症状**:
- **根因**:
- **解法**:
- **防御**:
- **关联**:
---
-->

1
.cursor/plans/.gitkeep Normal file
View File

@@ -0,0 +1 @@

View File

@@ -1,5 +1,15 @@
# Project Profile Log
详细记录每次项目画像更新的完整上下文,按时间正序追加。
详细记录项目画像的新增、演进和删除上下文,按时间正序追加。
## 记录
<!--
### [profile-change-id] 日期 — 标题
- **分类**:
- **精简版**:
- **原始上下文**:
- **来源对话**:
- **操作**:
-->

View File

@@ -1,9 +1,19 @@
# Project Profile
记录当前项目中稳定、可复用且已确认的事实。详细演进过程写入 `.cursor/profile/project-profile-log.md`
## 项目定位
<!-- 项目目标、用户与边界。 -->
## 技术栈与架构
<!-- 语言、框架、运行环境和关键模块。 -->
## 设计约定
<!-- 长期有效的代码组织、接口和数据约定。 -->
## 产品方向
<!-- 已确认的产品原则与优先级。 -->

View File

@@ -1,43 +1,35 @@
---
description: 跨项目底层通用约定语言、目录归属
description: 跨项目底层约定语言、目录归属、运行时版本与 Windows 文本编码
alwaysApply: true
---
## 语言约定
# 基础约定
Agent回复时使用简体中文。
代码注释时尽量使用简体中文。
写SKILL的时候文档部分也尽量使用简体中文。
专业术语和项目专有名词不受此语言限制,通常是英文则保留。
## 语言
## Rules / Skills 目录归属约定
- 面向用户的回复使用简体中文;代码注释和 Skill 文档优先使用简体中文。
- API、类型名、命令和项目专有名词保留其惯用英文写法。
本项目使用以下目录结构区分跨项目通用内容与项目专属内容:
## Rules / Skills 目录归属
### Rules
- 跨项目通用 Rule 放在 `.cursor/rules/common/`。
- 当前项目或特定技术栈 Rule 放在 `.cursor/rules/project/`。
- 项目内 Skill 放在 `.cursor/skills/`;全局副本只能作为可选共享副本,不能替代项目内版本。
- 新建 Rule 或 Skill 前先判断归属;无法判断时先询问用户。
| 目录 | 用途 | 示例 |
|------|------|------|
| `.cursor/rules/common/` | 跨项目通用规则,开新项目时可直接复制 | 语言约定、Orchestrator 触发、延期方案回忆 |
| `.cursor/rules/project/` | 当前项目专属规则 | 项目架构约定、框架特定规范、项目专属工作流 |
## 运行时版本
### Skills
创建环境、安装依赖、启动项目或运行测试前,先确认项目声明的运行时版本;禁止直接采用系统默认版本,也不得擅自升级到最新或预发布版本。
| 位置 | 用途 | 示例 |
|------|------|------|
| `.cursor/skills/` | **所有 Skill 的主存储位置**(含通用和项目专属) | epee-orchestrator、deferred-decisions、项目特定 Skill |
| `~/.cursor/skills/` | 跨项目通用 Skill 的全局副本(可选,方便其他项目复用) | epee-orchestrator、deferred-decisions |
- Godot检查 `project.godot` 的兼容声明、项目 README 的版本要求,以及 `.cursor/local-env.json` 记录的本机可执行文件与版本;本机配置只用于选择运行时,不能覆盖项目要求。
- Python检查 `pyproject.toml`、`.python-version` 和 README。
- Node.js检查 `package.json` 的 `engines`、`.nvmrc`、`.node-version` 和 README。
- 声明互相冲突时指出冲突;版本仍不明确时,先向用户确认再安装或执行。
- 本机版本不匹配时先报告并给出调整方案,不通过修改业务代码迁就错误环境。
> **重要**:无论 Skill 是通用还是项目专属,都**必须**在项目的 `.cursor/skills/` 下保留一份,
> 以确保能被 Git 管理和版本控制。全局目录 `~/.cursor/skills/` 仅作为跨项目共享的便利副本,
> 不作为唯一存储位置。
## Windows UTF-8 安全
### Agent 创建 Rule 或 Skill 时必须遵守
1. **先判断归属**:新建 Rule 或 Skill 前,评估其是否为跨项目通用内容
2. **如果不确定,必须询问用户**"这个 Rule/Skill 是通用的还是项目专属的?"
3. 确认后放入对应目录:
- 通用 Rule → `.cursor/rules/common/`
- 项目专属 Rule → `.cursor/rules/project/`
- **所有 Skill含通用→ `.cursor/skills/`**(必须,确保 Git 可管理)
- 通用 Skill 额外同步 → `~/.cursor/skills/`(可选,方便其他项目使用)
- 文本文件统一保存为 UTF-8 无 BOM、LF 换行;修改后按需校验 BOM 与换行。
- 优先使用 IDE 的文件读写工具,避免文本经控制台编码转换后再写回。
- 必须用 PowerShell 写文本时显式使用可产生 UTF-8 无 BOM 的 API并明确 LF不要依赖 Windows PowerShell 与 PowerShell 7 不同的默认编码。
- 必须查看含中文的命令输出时先将控制台输出编码设为 UTF-8若仍有乱码写入显式 UTF-8 的临时文件后再用文件工具读取。

View File

@@ -1,125 +1,38 @@
---
description: 每次会话开始时注入开发日志概要L3并在检测到任务与近期改动相关时自动读取中期记录L2
description: 会话中按需回忆开发日志,并在完成变更时维护多人 fragment、踩坑记录与 Skill registry
alwaysApply: true
---
## 开发日志上下文注入
# 开发日志回忆与收尾
每次会话处理用户第一个任务前,执行以下操作:
## 会话开始
1. 读取 `.cursor/changelog/changelog-headlines.md`不存在跳过
2. 如文件存在且有实质内容(不仅是模板头部),将全部条目作为背景知识注入上下文
3. 这些信息帮助 Agent 快速建立位置感:项目进展到什么阶段、最近的工作重心在哪个模块
4. 在后续回复中自然参考,不显式提及"根据开发日志"
1. 读取 `.cursor/changelog/changelog-headlines.md`不存在或只有模板时跳过
2. 当前任务与近期模块、标签或文件相关时,再读取 `.cursor/changelog/changelog-recent.md` 中的相关条目。
3. 条目仍有歧义时,按记录 ID 精确查找对应 fragment 或只读取 `changelog-full.md` 的局部范围;不要无条件加载完整历史。
4. 日志只作为背景事实使用,不用在回复中刻意声明来源。
## 主动定位辅助
## 变更日志写入
L3 概要的核心价值之一是帮助 Agent 在**冷启动**(新会话、无上下文)时理解用户意图。
当用户的请求缺少具体文件名或模块名时Agent 应主动利用 L3 进行推断:
完成有实质影响的代码、配置或文档变更后:
### 典型场景
1. 读取 `.cursor/skills/dev-changelog/SKILL.md` 并遵循其当前流程;文件不存在时说明无法记录,不自行发明格式。
2. 从 `.cursor/local-env.json` 获取稳定的 `changelog-author`;缺失时先询问用户。
3. 每次记录都新建独立 fragment`.cursor/changelog/entries/<author>/<id>.md`;不得复用或覆盖他人的 fragment以避免多人修改同一文件。
4. 不直接编辑生成视图;按 Skill 指定的生成器重建并校验视图。
1. **隐式延续**:用户说"继续做昨天那个"、"把那个功能完善一下"
→ 从 L3 中找到最近的相关条目,上溯到 L2 获取具体文件列表
2. **模糊指代**:用户说"那个组件有 bug"、"之前改的那个接口"
→ 用 L3 中的关键词匹配用户描述,定位到具体改动
3. **上下文补全**:用户直接提出一个任务,没有背景说明
→ 用 L3 判断该任务是否与近期某个改动有关联(如同一模块、同一功能线)
## 踩坑信号自检
### 流程
最终回复前快速判断本次任务是否出现了非显而易见的运行时、构建、配置、版本或工具链问题,尤其是:
```
1. 解析用户请求,识别是否存在隐式引用或模糊指代
2. 在 L3 概要中查找语义最匹配的 1-3 条记录
3. 提取匹配条目的锚点 ID上溯到 L2 获取 affected_files 和 tags
4. 如有必要,继续上溯到 L1 获取完整的决策背景
5. 将定位到的文件/模块作为任务的起点,开始执行
```
- 报错后经过排查才定位根因;
- 只看代码无法预见,实际执行才暴露;
- 同一问题在会话中重复出现。
如果 L3 中没有匹配到任何相关记录,正常处理即可——不是所有任务都与近期改动有关
命中时读取 `.cursor/skills/pitfall-journal/SKILL.md`,按其写入流程记录;纯拼写、显然语法错误或普通业务调整不记录。开发日志中的摘要不能替代可检索的踩坑记录
## L2 自动触发
## Skill 一致性
Agent 开始处理一个新任务时,判断是否需要读取近期详细记录:
1. 从当前任务中提取涉及的文件路径和语义关键词
2. 与 L3 概要中的内容做快速比对——如果近期有相关模块/文件的改动记录
3. 命中时,读取 `.cursor/changelog/changelog-recent.md`,将相关条目纳入上下文
4. 匹配策略:
- 硬匹配:当前任务涉及的文件出现在 L2 条目的 `affected_files` 中
- 软匹配:当前任务的语义关键词与条目的 `tags` 有交集
- 任一命中即触发读取
### 注意
- L3 注入是低成本操作(~50 句话),每次会话都执行
- L2 读取按需触发,只在检测到关联时才读取
- 开发日志是事实性记录,直接使用即可,不像画像那样需要"自然融入"的措辞考量
- 记录的写入和管理由 `dev-changelog` Skill 负责,本 Rule 只负责读取和注入
## 逐级上溯
当 L3 中某条记录的一句话描述**语义模糊**(无法判断具体范围或与当前任务的关系),
按以下步骤精准上溯,**禁止全文读取 L1**
1. 提取该条目的锚点 ID`CL-xxx`
2. 用 Grep 在 `changelog-recent.md`L2中搜索该 ID → 找到则读取该条目
3. 如 L2 中未找到或仍有歧义 → 用 Grep 在 `changelog-full.md`L1中搜索该 ID
获取行号后用 Read 工具读取该行号 ±20 行范围
4. 一次上溯通常只涉及 1-3 条记录,不批量上溯
## 任务完成 Checklist强制
Agent 在即将输出最终回复前,**必须**逐项检查以下清单。
这是硬性要求,不是建议——**跳过任何一项都视为执行错误**。
### 前置项(每次回复前无条件执行)
**A. 画像信号扫描(短路版)**
目的:以最小 token 成本维持 `profile-memory` Skill 的被动检测通路。
步骤:
1. **快判(词表匹配)**:扫描本次对话中**用户消息**,检查是否**明显**出现以下偏好/决策触发词族之一:
- 个人偏好信号:`我喜欢` / `我倾向` / `我不喜欢` / `我讨厌` / `以后都` / `以后别` /
`别再` / `不要再` / `我的风格` / `我的偏好` / `太花哨` / `minimal` /
`简洁` / `精炼` / `啰嗦`
- 项目决策信号:`这个项目是` / `目标用户是` / `技术栈` / `架构上` /
`产品方向` / `定位是`
2. **分支**
- 无匹配 → **直接跳过**,进入下一 checklist 项thinking 应在 50 token 内结束)
- 有匹配 → 读取 `.cursor/skills/profile-memory/SKILL.md` 并执行操作 A 的确认流程
3. **约束**:本项不做全量语义判断——漏记隐式信号是可接受的权衡。
用户如需记录隐式偏好可显式说"把这条记入画像"。
### 变更检查项(仅当本次有代码/配置文件改动时执行)
1. **本次是否有代码/配置文件改动?**(不含 `.cursor/` 下的基础设施文件)
- 否 → 跳过本组后续项,直接进入"收尾项"
- 是 → 继续
2. **是否已写入三层开发日志?**
- 否 → **立即**读取 `dev-changelog` Skill 并执行操作 A写入 L1 + L2 + L3 后再输出回复
- 是 → 继续
3. **是否修改了任何 Skill**(包括 SKILL.md 内容变更)
- 否 → 跳过
- 是 → 检查是否已同步 `epee-orchestrator` 的 `registry.md`,未同步则立即执行
4. **本次是否有 Skill 使用异常?**(遗漏、误触、产物与用户预期偏差)
- 否 → 跳过
- 是 → 执行 `epee-orchestrator.mdc` 中的自迭代确认流程
### 收尾项(每次回复前无条件执行)
**Z. 静默 Hook ack**
- 用 Write 工具创建空文件 `.cursor/changelog/.changelog-ack`
- 这会更新 ack 文件的 mtime阻止 `check-changelog.ps1` hook 在后续回复中重复提醒
**执行原则**:先完成 checklist 中的所有写入动作,再输出面向用户的回复文本。
回复末尾如果执行了变更检查项第 2 项,附 `[已记录到开发日志]`。
- 创建、删除、重命名或修改任何 Skill 后,同步 `.cursor/skills/epee-orchestrator/registry.md`,确保名称、入口、能力描述与触发条件一致。
- Skill 使用出现遗漏、误触或系统性偏差时,按 `epee-orchestrator` Skill 的自迭代流程处理。
- 用户明确限制可修改路径时,以该限制为准:报告未能执行的日志或 registry 写入,不越权修改。

View File

@@ -1,79 +1,29 @@
---
description: 引导 Agent 在首次会话自动、后续通过关键词手动触发 cursor-init Skill
globs:
description: 首次会话检查 Cursor 初始化状态,并响应显式初始化或重置请求
alwaysApply: true
---
## 首次初始化自检(最高优先级 · 必须最先执行)
# Cursor 初始化触发
**每一次会话的首次用户消息响应之前**Agent 必须先检查工作区是否存在 `.cursor/.init-done`
## 首次会话自检
### 判定逻辑
处理每次会话的第一个用户任务前,检查 `.cursor/.init-done`
```
IF .cursor/.init-done 不存在:
→ 本仓库是"clone 模板后的首次会话"
→ 必须在处理用户原始请求之前先完成 cursor-init
ELSE:
→ 已 init 过,跳过本自检
→ 仅当用户命中下方"手动触发关键词"时才再次执行本 Skill
```
- 文件存在:正常处理任务;没有显式触发词时不重复提醒。
- 文件不存在:说明仓库尚未完成 Cursor 初始化,概述原任务,并在执行清理、移动、覆盖等操作前取得用户确认。
- 用户要求跳过初始化时,继续原任务并简短提示下次会话仍会检查;不得强制初始化。
### 首次触发的执行方式
用户确认后读取 `.cursor/skills/cursor-init/SKILL.md`严格按其当前阶段执行。Skill 不存在时停止初始化并报告缺失,不自行实施破坏性替代流程。
1. **暂存用户原始请求**,明确告诉用户:
> "检测到当前仓库刚从模板 clone 下来,还未完成 .cursor 初始化。需要先跑一遍 cursor-init
> (清理模板遗留数据、配置项目画像、可选生成 .gitignore。完成后再处理你的请求{原始请求概要}。"
2. 等用户明确回复"继续/OK"后,读取 `.cursor/skills/cursor-init/SKILL.md` 并严格执行阶段 0 → 7
3. **init 完成后**(阶段 7 写入 `.init-done` 成功后),回到被暂存的用户原始请求继续处理
4. 若用户说"先不做 init就处理我的请求"
- 尊重用户选择,跳过本次 init
- 但仍在本次回复中明确说明 sentinel 缺失,并提示"下次会话还会再次提醒"
## 手动触发
### 自检成本说明
以下意图触发完整初始化或重置:`初始化 cursor`、`cursor init`、`重置 cursor`、`reset cursor`、清理复制来的 `.cursor`。
已 init 项目:每次会话仅多一次 `Read .cursor/.init-done`(极低成本)
若 Read 失败或路径不存在,即按"首次"处理。
以下意图仅触发 Skill 中的 `.gitignore` 流程:`补 gitignore`、`生成 .gitignore`、`gitignore 模板`
---
## 执行约束
## 手动触发关键词(任意时刻,已 init 仓库也可用)
### A. 全流程重置(阶段 0 → 7
匹配任一:
- "初始化 cursor" / "初始化cursor" / "cursor 初始化"
- "重置 cursor" / "reset cursor"
- "cursor init" / "init cursor"
- "把复制过来的 .cursor 清理一下" / "按 baserule 归位一下"
**执行方式**:读取 `.cursor/skills/cursor-init/SKILL.md`,按阶段 0 → 7 执行。
若 `.cursor/.init-done` 已存在,阶段 0 会展示其元数据并要求用户二次确认。
### B. 仅补 .gitignore只跑阶段 5.5
匹配任一:
- "补 gitignore" / "补一下 gitignore" / "补个 gitignore"
- "生成 gitignore" / "生成 .gitignore"
- "gitignore 模板" / "来份 gitignore"
**执行方式**:读取 SKILL.md 的**阶段 5.5 章节**单独执行,不触碰其他阶段。
完成后更新 `.init-done` 中的 `gitignore_generated: true`。
---
## 执行原则
1. **稳定准确优先于 token 成本**
2. 破坏性操作(删除、重置、移动)前必须给用户 dry-run 清单
3. 分类不清的文件必须逐条询问用户,不要猜
4. 开始前先执行 `git status` 并提醒用户 commit/stash
5. 设计为幂等——重复运行在已干净状态下不应造成破坏
---
## 不触发本 Skill 的情况
- 用户只是问"cursor 有什么 skill" —— 这是浏览需求,走 `epee-orchestrator` 的操作 A
- 用户只是想初始化某个具体数据文件(如"初始化画像")—— 走对应 Skill`profile-memory` 等),不涉及全局重置
- `.cursor/.init-done` 存在 且 用户请求中**没有**任一 A/B 组关键词 —— 正常响应用户,不提 init
- 开始前检查工作区状态,提醒用户处理未提交改动。
- 删除、重置、覆盖和批量移动前提供 dry-run 清单并取得确认。
- 分类不清的文件逐项询问,不猜测归属。
- 流程保持幂等;重复执行不应破坏已经正确的状态。

View File

@@ -1,21 +1,16 @@
---
description: 每次会话开始时扫描延期方案记录,在任务与已有 deferred item 关联时主动提醒用户
description: 会话开始时按任务关联度回忆尚未处理的延期方案
alwaysApply: true
---
## 延期方案主动回忆
# 延期方案主动回忆
每次会话处理用户第一个任务前,执行以下检查
处理会话中的第一个任务前
1. 读取 `.cursor/deferred/registry.md`不存在跳过
2. 扫描所有 `status: deferred` 的条目
3. 将每个条目的 **tags** 和 **related_files** 与当前任务的关键词/文件做匹配
4. 如果匹配到关联条目,在回复开头简要提醒:
> 提醒:你之前有一个延期方案 **[标题]** 与当前任务相关tags: xxx。要一并处理吗
5. 每个条目每次会话最多提醒一次,不重复打扰
1. 读取 `.cursor/deferred/registry.md`不存在或为空时跳过
2. 只考虑仍处于 `deferred` 状态的条目
3. 条目的 `tags`、`related_files` 和当前任务的关键词、目标文件做匹配
4. 命中时用不超过两行提醒标题与关联原因,并询问是否一并处理;不要擅自扩大当前任务范围。
5. 同一条目每次会话最多提醒一次,未命中时保持静默。
### 注意
- 只匹配 `status: deferred` 的条目(`reminded` / `in_progress` 不再提醒)
- 提醒应简洁,不超过 2 行,不打断用户主线任务
- 具体的记录/管理操作请参考 `deferred-decisions` Skill
新增、更新或关闭延期方案时,读取 `.cursor/skills/deferred-decisions/SKILL.md` 并按其格式执行Skill 不存在时只报告,不自行发明记录格式。

View File

@@ -0,0 +1,20 @@
---
description: 约束外部依赖、插件与第三方资源的引入和升级
alwaysApply: true
---
# 依赖与外部资源治理
引入、下载、安装或升级任何外部依赖前,必须先获得用户确认,包括:
- 引擎插件、addon、扩展和原生库
- npm、pip 等包管理依赖及锁文件变更;
- Asset Library 或网络来源的素材、字体、音频、模型和模板;
- 会改变构建、导出或运行环境的工具链组件。
确认前说明:名称、用途、目标版本、来源、许可、维护状态、影响范围,以及工程内是否已有替代方案。
- 来源或许可不明确时默认不引入。
- 优先复用工程已有能力;不得为便利而复制功能重叠的依赖。
- 升级前检查项目声明的运行时兼容范围和变更说明。
- 用户拒绝或暂未确认时,提供无新增依赖的方案,不静默安装。

View File

@@ -1,44 +1,27 @@
## Problem Distillery 上下文注入
---
description: 会话注入已蒸馏经验,并被动识别反复未解决的问题
alwaysApply: true
---
### Golden Rules 注入
# Problem Distillery 回忆
每次会话处理用户第一个任务前,执行以下操作:
## 会话注入
1. 读取 `.cursor/distillery/golden-rules.md`(不存在则跳过)
2. 如文件存在且有实质条目(不仅是标题),将全部条目作为背景知识注入上下文
3. 这些是经过实践反复验证的精炼认知Agent 在分析和解决问题时应自然参考
4. 不需要在回复中显式提及"根据 Golden Rule"
处理会话中的第一个任务前:
### 蒸馏提醒检查
1. 读取 `.cursor/distillery/golden-rules.md`;不存在或只有模板时跳过。
2. 将有效条目作为解决问题的背景约束自然应用,不必显式声明来源。
3. 读取 `.cursor/distillery/insights.md` 的蒸馏日期;距今超过 7 天时,按需检查 `.cursor/distillery/problems.md`。
4. 若存在已解决但尚未蒸馏的条目,最多提醒一次并询问用户是否现在整理。
每次会话处理用户第一个任务前,额外检查:
## 被动触发
1. 读取 `.cursor/distillery/insights.md`(不存在则跳过)
2. 检查文件尾部的 `last_distill_date` 字段
3. 如果距今超过 7 天,读取 `.cursor/distillery/problems.md`
4. 统计 `status: resolved` 且无 `distilled:` 标记的条目数量
5. 如有未蒸馏的已解决条目,提醒用户:
> 你有 N 个已解决的顽固问题尚未总结,要花几分钟蒸馏一下吗?
6. 每次会话最多提醒一次
整个会话中持续关注以下信号:
### 顽固问题检测
- 用户明确表示问题仍未解决或再次出现;
- 同一问题采用两个方案后仍失败;
- 已记录的踩坑再次出现且没有稳定解法。
Agent 在整个对话过程中应保持对以下信号的被动感知:
命中时读取 `.cursor/skills/problem-distillery/SKILL.md`,按其追踪流程执行,并利用已有 insights 做相关经验验证。没有命中时不要读取完整 `problems.md`。
1. 用户表达问题未解决:"还有问题"、"没解决"、"还是一样"、"又出现了"、"不行"、"没用"
2. Agent 自身意识到同一问题已尝试 2 次以上仍未解决
3. pitfall-journal 中已有记录的问题再次出现
检测到上述信号时,读取 `problem-distillery` Skill 并执行其操作 A。
### 验证触发
当操作 A 触发时(追踪新的或再次出现的顽固问题),如果 `insights.md` 非空,
还应执行操作 D 的被动验证流程——匹配是否有相关的已蒸馏经验可供参考。
### 注意
- Golden Rules 注入是极轻量操作(预期 < 30 行),每次会话都执行
- 蒸馏提醒按需触发,只在条件满足时提醒
- problems.md 的详细记录**不主动读取**,仅在操作 A/B/C 时按需读取
- 本 Rule 只负责触发和注入,具体操作流程由 `problem-distillery` Skill 定义
文件或 Skill 不存在时静默跳过读取;需要写入而 Skill 缺失时报告缺失,不自行创造结构。

View File

@@ -1,70 +1,31 @@
---
description: >-
EPEE Skill Orchestrator 触发入口。检测当前任务是否可由已有 Skill 处理,
或是否值得创建新 Skill。同时管理 Skill 变更后的 Registry 同步和自迭代经验积累。
description: 检测可复用 Skill 场景,并维护 Skill registry 与使用经验
alwaysApply: true
---
## EPEE Skill Orchestrator — 触发雷达
# Skill Orchestrator 触发
### 被动检测
制定方案或执行任务时,如出现任一情况,读取 `.cursor/skills/epee-orchestrator/SKILL.md` 并按其分流:
Agent 在制定方案或执行任务过程中,如果发现当前任务符合以下**任意一条**特征,
必须读取 `epee-orchestrator` Skill 的 SKILL.md 并执行其分流流程:
- 需要大量人工配置或逐项填写参数;
- 同结构操作预计会重复发生;
- 即将给出超过 10 步的纯手工操作;
- 用户询问现有 Skill、希望复用能力或创建新 Skill。
1. **手动配置密集**:任务需要用户在 IDE 或工具中进行大量手动配置
(如批量填写配置字段、逐一调整参数等)
2. **重复模式明确**:同类操作预计会反复出现(如批量创建同结构的配置文件、
批量设置同类模块等)
3. **手工指引过长**Agent 发现自己正在生成超过 10 步的"手动操作步骤"
而非直接产出代码或配置文件
## Registry 同步
### Registry 同步(强制)
创建、删除、重命名或修改任何 Skill 后:
每次**创建**或**修改**任何 Skill包括 SKILL.md 内容变更、新增 Skill 等)后,
Agent **必须**执行以下操作:
1. 更新 `.cursor/skills/epee-orchestrator/registry.md` 中的对应条目。
2. 使名称、能力说明、适用场景和入口路径与实际内容一致。
3. 不保留指向已删除 Skill 的条目。
1. 读取 `epee-orchestrator` Skill 目录下的 `registry.md`
2. 更新或新增对应 Skill 的条目(格式参见 registry.md 中的条目结构)
3. 确保条目中的能力描述和触发场景与 Skill 实际内容一致
## 自迭代
> **注意**:此项已纳入 `changelog-recall.mdc` 的"任务完成 Checklist"第 3 项。
> 如果 Agent 在 checklist 阶段发现遗漏,必须立即补执行。
使用 Skill 时若因信息缺失导致失败、用户反复补充同类信息,或产物持续偏离预期:
### Skill 自迭代(强制)
1. 提炼一条具体、可执行的前置检查。
2. 先向用户确认是否写入该 Skill 的自迭代记录。
3. 用户同意后再更新 Skill并同步 registry。
每个 SKILL.md 必须包含一个"自迭代日志"章节,用于记录使用该 Skill 过程中发现的经验教训
**触发条件** — 在创建或使用任何 Skill 时,遇到以下情况应触发自迭代流程:
1. 因信息缺失导致生成结果错误或构建失败
2. 用户需要反复补充同类信息
3. 生成产物与用户预期存在系统性偏差
> **注意**:此项已纳入 `changelog-recall.mdc` 的"任务完成 Checklist"第 4 项。
> Agent 不应等到"下次使用 Skill 时"才想起自迭代——当次就应检查。
**流程**
1. 识别问题根因,归纳为一条简明的检查项
2. 向用户确认:"是否要将此项记录到该 Skill 的自迭代日志中?"
3. 用户同意后,追加到对应 SKILL.md 的"已知必要检查"列表
4. 后续使用该 Skill 时,必须遵守日志中已记录的所有检查项
**SKILL.md 中的格式**
```markdown
## 自迭代日志
本节记录使用本 Skill 过程中发现的必要检查项。
### 已知必要检查
1. **检查项名称** — 简要说明原因和应对方式。
```
**原则**
- 每条检查项应当**具体可执行**,而非泛泛的提醒
- 检查项只增不删,除非用户明确要求移除
- 新建 Skill 时须预置空的自迭代日志章节
用户限制可修改范围时不得越权;只报告待同步项

View File

@@ -1,31 +1,29 @@
## 踩坑经验自动检索
---
description: 在 Debug、运行时错误或重复失败时被动检索既有踩坑经验
alwaysApply: true
---
### 被动检测触发
# 踩坑经验被动检索
Agent 在以下场景中,应自动读取 `.cursor/pitfalls/pitfalls.md` 并进行匹配检索:
## 触发条件
1. **进入 Debug mode**:读取全部条目,将当前错误症状与已有记录比对
2. **遇到运行时错误**:提取错误信息关键词,在"症状"字段中检索匹配
3. **同一问题第二次出现**:如果当前会话中某个错误已出现过一次且未解决,强制检索
出现任一情况时读取 `.cursor/pitfalls/pitfalls.md`
### 匹配策略
- 进入 Debug 或系统化故障排查;
- 遇到运行时、构建、配置、版本或工具链错误;
- 同一问题在当前会话中第二次出现。
```
1. 提取当前问题的信号:错误信息关键词、涉及文件/模块、技术栈
2. 在 pitfalls.md 中匹配:
- 硬匹配:错误关键词出现在条目的"症状"中
- 软匹配:模块/技术栈出现在条目的"关联"中
3. 命中时在分析开头提示:
> 注意:之前遇到过类似问题 [PF-xxx][标题]。根因是 [xxx],先排查这个方向。
```
文件不存在或只有模板时跳过,不报错。
### 写入提醒
## 匹配方式
Agent 在完成涉及 debug/修复的任务后,应读取 `pitfall-journal` Skill 并执行其"操作 A写入记录"流程
判断标准:问题的根因是否"非显而易见"——如果只看代码逻辑觉得应该没问题,但实际运行时才暴露,就值得记录
1. 提取错误关键词、涉及模块、运行环境和复现条件
2. 优先匹配条目的“症状”,再匹配“关联”或技术栈
3. 命中时简短指出相似记录、既有根因和首要排查方向。
4. 将命中结果视为假设,必须用当前证据验证;同一条记录每次会话最多提醒一次。
### 注意
## 任务结束
- 检索结果是**辅助参考**,不是确定性答案——匹配到不代表根因一定相同
- pitfalls.md 不存在时跳过,不报错
- 每次会话中对同一条 pitfall 最多提醒一次
若根因只能通过实际运行或多步排查发现,读取 `.cursor/skills/pitfall-journal/SKILL.md` 并按其流程记录。纯拼写、明显语法错误或普通业务逻辑调整不记录。
用户限制可修改范围或 Skill 缺失时,不越权写入,只报告待记录事项。

View File

@@ -1,46 +1,23 @@
---
description: 每次会话开始时读取用户画像和项目画像,将精简 Profile 注入上下文以指导 Agent 行为
description: 会话开始时读取用户项目画像,并被动识别值得确认的新偏好
alwaysApply: true
---
## 画像上下文注入
# 画像回忆
每次会话处理用户第一个任务前,执行以下操作
处理会话中的第一个任务前
1. 读取 `~/.cursor/profile/user-profile.md`(不存在记为 user_missing
2. 读取 `.cursor/profile/project-profile.md`(不存在记为 project_missing
3. **冷启动检测**:如果 user_missing 或 project_missing 为真,在回复开头简要提醒:
> 画像系统尚未初始化缺少user-profile / project-profile
> 如需启用画像功能,请说"初始化画像",我会引导你完成。
- 每次会话最多提醒一次,不重复打扰
- 如用户回应"初始化画像",读取 `profile-memory` Skill 并按其模板创建文件,
然后引导用户填写基本信息
4. 如文件存在且有实质内容(非空模板),将其内容作为背景知识纳入考量
5. 在后续回复中Agent 应自然地参考画像信息,无需显式引用
1. 读取 `~/.cursor/profile/user-profile.md` 和 `.cursor/profile/project-profile.md`。
2. 文件缺失时最多提醒一次,并在用户希望初始化时读取 `.cursor/skills/profile-memory/SKILL.md`。
3. 有效画像仅作为背景参考自然应用;当前用户指令与画像冲突时,以当前指令为准。
4. 精简画像条目有歧义时,按其锚点在对应日志中精确查找,只读取必要局部,不全文加载历史
### 注意
## 被动识别
- 画像信息是背景参考,不是硬性约束——当用户当前指令与画像冲突时,以当前指令为准
- 不要在回复中提及"根据你的画像"之类的措辞,自然融入即可
- 画像的记录和管理由 `profile-memory` Skill 负责,本 Rule 只负责读取和注入
用户明确表达长期偏好、长期禁忌、固定工作方式,或项目定位、目标用户、技术栈与架构决策时:
### 逐级上溯
1. 区分本次临时要求与可跨会话复用的信息。
2. 对可能长期有效的信息,读取 `profile-memory` Skill 并先向用户确认是否记录。
3. 未经确认不得把推测写入画像;隐式信号不强行记录。
当精简版 Profile 中某条记录**语义模糊**(无法判断偏好的具体适用场景),
按以下步骤精准查找详细 Log**禁止全文读取 Log 文件**
1. 提取该条目的锚点 IDHTML 注释中的 `PF-xxx`
2. 用 Grep 在对应的 Log 文件中搜索该 ID获取行号
3. 用 Read 工具读取该行号 ±15 行范围,获取原始上下文和来源信息
4. 一次上溯通常只涉及 1-2 条记录,不批量上溯
## 被动检测触发点
被动检测不在本 Rule 执行,而是由 `changelog-recall.mdc` 的
**任务完成 Checklist 前置项 A画像信号扫描** 统一触发。
理由:本 Rule 仅负责上下文注入(低成本、每次必做);被动检测需要挂入
已有强制力的 checklist 中才能避免被 Agent 遗漏,该 checklist 由
`changelog-recall` 维护,两者分工清晰。
具体触发词族和短路逻辑详见 `changelog-recall.mdc` 中的前置项 A 定义。
文件或 Skill 不存在时只报告缺失,不自行创建不兼容格式。

View File

@@ -0,0 +1,41 @@
---
description: Godot 项目的分层、依赖方向、信号解耦与工程完整性约定
alwaysApply: true
---
# Godot 分层架构
先识别项目已有目录、autoload 和通信方式,再沿用其边界;不要仅为套用规则而创建新框架。
## 依赖方向
- 领域逻辑负责规则、状态与数据,尽量保持可在 headless 环境运行。
- 表现层负责场景、UI、输入、动画、音频和特效不在回调中复制领域规则。
- 逻辑层不得加载 UI 场景、抓取表现节点或等待表现动画完成。
- UI 将用户意图转为逻辑层公开命令;状态变化通过返回值、可观察状态或信号反馈。
- 跨系统优先用明确接口或信号解耦。若项目已有事件总线,复用其契约;没有时不要强制新建全局总线。
```gdscript
# 表现层只转发意图
func _on_confirm_pressed() -> void:
entity_service.confirm(entity_id)
# 逻辑层发布状态变化,不直接操作 UI
func confirm(id: StringName) -> void:
# 更新领域状态
entity_confirmed.emit(id)
```
## 边界约束
- 信号参数和公开方法是跨层契约;修改时同步调用方、测试与相关文档。
- 避免双向依赖和跨层节点路径。依赖应显式注入或通过稳定接口获取。
- autoload 只承载确需全局生命周期的服务或状态,不把普通功能默认做成单例。
- 新系统先明确所有权、生命周期、可测试边界和失败处理,再决定放置位置。
## 工程完整性
- Godot 版本遵循项目声明;升级引擎或兼容特征前必须说明影响并取得确认。
- 修改 `project.godot`、导出预设、autoload 或输入映射前,先说明运行与构建影响。
- Godot 生成的 UID 与对应资源保持一致,不手工伪造;移动或删除资源时检查引用。
- 外部插件和第三方资源遵守 `dependency-governance`。

View File

@@ -0,0 +1,26 @@
---
description: Godot 素材引用、导入一致性与可替换占位规范
globs: "**/*.{gd,tscn,tres,res}"
alwaysApply: false
---
# Godot 素材与占位
## 正式素材
- 面向用户的成品视觉应使用项目批准的正式素材;程序绘制只用于确属程序化的视觉、调试或明确的临时占位。
- 沿用项目现有素材目录、命名、尺寸基准和导入预设,不假设固定路径或分辨率。
- 文件命名保持稳定、可搜索;移动或重命名后检查场景、资源和脚本引用。
- 纹理过滤、mipmap、压缩、循环和采样等导入设置按素材用途确定同类素材保持一致。
- 逻辑层只引用稳定的资源标识或数据,不直接依赖表现素材。
## 缺失素材占位
正式素材缺失时可以占位,但必须满足:
- 视觉上明显区别于成品,并显示或记录可搜索的 asset key
- 在代码或清单中标注 `PLACEHOLDER`,注明期望用途和替换条件;
- 占位节点尺寸、锚点和接口与目标素材兼容,替换时不要求改业务逻辑;
- 项目已有统一占位生成器或缺失素材清单时必须复用;没有时采用最小可删除实现,不额外建立复杂系统。
提交或交付前搜索未解决的 `PLACEHOLDER`,明确哪些仍被允许保留。

View File

@@ -0,0 +1,28 @@
---
description: Godot 数据驱动内容、数据访问、校验与存档兼容约定
globs: "**/*.{gd,tres,res,csv,json}"
alwaysApply: false
---
# Godot 数据系统
## 数据驱动边界
- 需要由策划或内容人员独立调整的参数、实体定义和效果配置,应放入项目采用的数据源,不散落为脚本字面量。
- 先复用项目已有的 `Resource`、CSV、JSON 或其他数据管线;项目尚未采用数据驱动时,根据实际复用需求选择最小方案,不强制新增格式或仓库层。
- 技术常量、枚举、协议版本和与内容无关的安全默认值可以留在代码中,并用名称或注释说明用途。
- 不明确某个数值属于内容还是技术约束时,先查现有数据定义和调用方。
## 加载与访问
- 为数据定义稳定 ID、字段类型、必填项和默认值加载边界统一做格式与引用校验。
- 解析失败应包含数据源、记录 ID 和字段名;可恢复问题使用安全默认值并报告,不可恢复问题尽早失败。
- 若项目已有 Store、Repository 或缓存层,公开查询入口统一经过该层,并提供测试可用的清理或重载能力。
- 调用方不应重复解析同一数据源,也不应依赖数据文件中的偶然顺序。
- 新增可配置行为时优先扩展既有注册或组合机制,避免在多个调用点堆叠同类条件分支。
## 存档与兼容
- 持久化数据携带明确版本;读取时对缺失字段提供默认值,并为不兼容结构提供显式转换。
- 只保存恢复状态所需的数据,不序列化场景节点引用或临时缓存。
- 数据结构变化时同时更新加载、保存、校验和回归测试。

View File

@@ -0,0 +1,35 @@
---
description: 通用 GDScript 文件结构、命名、类型、注释与错误处理规范
globs: "**/*.gd"
alwaysApply: false
---
# GDScript 风格
## 文件结构
按 Godot 风格保持稳定顺序:`@tool`(如需)→ `class_name`(如需)→ `extends` → 文档注释 → signals → enums → constants → exports → public members → private members → lifecycle → public methods → private methods。
- 可复用类型使用 `class_name`;仅由 autoload 注册的脚本避免重复声明同名全局类。
- 缩进使用 Tab单个文件内不得混用 Tab 与空格。
- 一个脚本聚焦一个主要职责;脚本持续膨胀时按状态、数据访问或表现职责拆分。
## 命名
- 文件、变量、函数、参数和信号使用 `snake_case`。
- 类名使用 `PascalCase`,常量使用 `UPPER_SNAKE_CASE`。
- 私有成员和私有方法使用 `_` 前缀;参数与成员冲突时使用 `p_` 前缀。
- 信号名称表达已经发生的事件,如 `entity_selected`,避免含糊的 `changed`。
## 类型与接口
- 函数参数和返回值显式标注类型;局部变量优先 `:=` 推断,容器尽量声明元素类型。
- 跨系统边界避免无结构的 `Dictionary`可复用数据优先使用具名类型、Resource 或明确 schema。
- 可空值、失败返回和副作用必须从签名、名称或文档中可见。
- 只对已理解且范围最小的问题使用 `@warning_ignore`,不全局压制告警。
## 注释与诊断
- `##` 用于公开类型、属性和方法的文档;普通注释解释原因与约束,不复述代码。
- 日志和警告包含稳定的模块或类标签以及关键上下文。
- 可恢复的外部数据问题用明确错误信息处理;真正的内部不变量可用 `assert`,不要用断言处理正常用户输入。

View File

@@ -0,0 +1,32 @@
---
description: Godot 场景节点组织、依赖、生命周期与用户文本约定
globs: "**/*.tscn"
alwaysApply: false
---
# Godot 场景与节点
## 场景组织
- 场景保持单一职责;可复用子结构拆成独立场景,并尽量能单独实例化验证。
- 场景拥有专用脚本时采用可对应的命名,例如 `entity_view.tscn` 与 `entity_view.gd`;不要求无脚本场景创建空脚本。
- 节点名称表达角色而非临时布局位置;不要依赖自动生成或易变化的名称作为跨场景契约。
- 重复子场景使用稳定的 `PackedScene` 引用;避免在运行热点反复同步加载资源。
## 节点依赖
- 场景内部的稳定子节点可用唯一节点名或局部路径引用。
- 外部依赖通过 `@export`、初始化方法或父级组装显式注入。
- 禁止硬编码场景树根路径或多级上溯路径来获取其他系统节点。
- 子节点向上报告事件使用信号;父节点向下调用明确方法。若项目已有总线,只用于真正跨系统的事件。
## 生命周期
- Node 释放使用 `queue_free()`;访问可能已释放的对象前检查有效性。
- 连接到长生命周期发射者的回调应在退出树时断开,或使用可随接收者失效的安全连接方式。
- `_process` 与 `_physics_process` 只做每帧必需工作;可事件驱动的更新不要轮询。
- 高频实例化是否使用对象池由性能测量决定,不预先为普通节点增加池化复杂度。
## 用户文本
用户可见文本集中在表现层,并接入项目已有本地化方案;不要在深层逻辑中硬编码最终文案。

View File

@@ -0,0 +1,29 @@
---
description: Godot 测试脚本的 headless、隔离、确定性与退出码规范
globs: "**/*test*.gd"
alwaysApply: false
---
# Godot 测试
## 运行原则
- 领域逻辑应能在无窗口、无 UI 和无渲染依赖的环境中验证。
- 运行前按项目声明确定 Godot 版本、可执行文件和测试入口;不假设固定目录、场景名或脚本命名。
- 项目已有测试框架或统一入口时必须复用。没有时采用最小可执行入口;引入第三方框架前遵守依赖确认规则。
- 自动化测试成功返回退出码 0失败返回非零不得仅依赖日志文本判断结果。
- 性能与超时断言应测量被测逻辑本身,避免输出管道、编辑器启动和资源导入污染计时。
## 用例约束
- 每个用例独立设置并清理状态,不依赖执行顺序或前一个用例留下的 autoload、缓存、随机种子和文件。
- 随机、时间和异步流程使用可控种子、可注入时钟或明确超时,避免无限等待。
- 优先断言公开行为、信号参数和持久化结果,不绑定私有实现细节。
- 纯逻辑用例不实例化 UI、3D 或特效场景;只有验证集成边界时才加载必要场景。
- 失败信息包含期望值、实际值和关键实体标识,便于在 headless 日志中定位。
## 变更要求
- 修复缺陷时先补可复现用例,或说明为何无法自动化。
- 新增逻辑系统时覆盖正常路径、边界输入、失败路径和状态重置。
- 数据 schema、存档版本、信号契约或项目配置变化时同步更新相关回归测试。

View File

@@ -1,830 +1,199 @@
---
name: cursor-init
description: >-
初始化项目的 .cursor 结构。两种触发方式:
1首次自动触发 —— 当 `.cursor/.init-done` sentinel 不存在时,`cursor-init-recall.mdc`
会在会话首次响应前引导 Agent 主动执行本 Skill
2关键词手动触发 —— "初始化cursor"、"cursor init"、"重置 cursor"、"补 gitignore" 等
流程:识别并迁移目标项目原有的 rules/skills清理模板残留的项目专用数据
changelog/deferred/pitfalls/profile/distillery/local-env/项目级 rules 与 skills
可选生成 .gitignore 模板,最后写入 `.cursor/.init-done` 标记完成。
已 init 仓库不会再自动触发,除非用户主动通过关键词请求。
初始化或重置项目的 .cursor 基础设施:保留 canonical Rules、Skills、Hooks 与
Godot baseline清除模板历史数据采集本机环境生成 .gitignore并在验证成功后
写入 .init-done。首次缺少 sentinel 时自动使用;也用于“初始化 cursor”、
重置 cursor”、“补 gitignore”等请求
---
# Cursor Init Skill
# Cursor Init
从其它项目复制过来(或通过 git clone 模板仓库获得)的 `.cursor/` 目录重置为当前新项目的干净起点
模板仓库或复制来的 `.cursor/` 变成可安全使用、可重复执行的 Godot 项目基线
## 使用场景
详细白名单、清理模式、状态模板、sentinel 和 `.gitignore` 片段统一见
[reference.md](reference.md)。执行全流程或仅补 `.gitignore` 前都必须直接读取该文件,
不得凭记忆重建清单。
### 场景 A克隆模板仓库后的首次会话
```
1. 用户把本模板作为 git 仓库发布
2. 其他人 `git clone <模板仓库>` 得到新项目骨架
3. 用户打开 Cursor在该仓库发出任意第一条消息
4. `cursor-init-recall.mdc`alwaysApply检测 .cursor/.init-done 不存在
→ 引导 Agent 暂停用户原始请求,先执行本 Skill
5. Agent 按阶段 0 → 7 跑完,写入 .init-done
6. 后续会话 sentinel 已存在,不再自动触发
```
## 入口
### 场景 B从已有项目直接复制 .cursor 目录
### 首次自动初始化
```
1. 用户在 "源项目 A" 中使用本 .cursor 模板开发
2. 用户开 "新项目 B",把 A 的 .cursor/ 整体拷贝到 B
3. B 原先可能也有自己零散的 rules/SKILL 文件(未必标准)
4. 用户在 B 中说"初始化cursor"(或首次会话被 recall 触发)
5. 本 Skill 把 B 原有的 rules/skills 按 baserule 归位,清空 A 遗留的项目数据,
重建 registry生成 .gitignore写入 sentinel
```
每次会话首次响应前由 `.cursor/rules/common/cursor-init-recall.mdc` 检查
`.cursor/.init-done`。若 sentinel 不存在:
### 场景 C只补 .gitignore
1. 暂存用户原请求,说明需要先初始化。
2. 获得用户同意后执行本 Skill 的阶段 07。
3. 仅在阶段 6 全部通过后写 sentinel。
4. 初始化成功后继续原请求。
```
init 后用户说"补一下 gitignore"或"生成 gitignore" → 只跑阶段 5.5
```
## 触发条件
本 Skill 由 `cursor-init-recall.mdc` 触发,两类入口:
若用户暂时拒绝,尊重选择;不要写 sentinel下次会话仍会提醒。
### A. 全流程(阶段 0 → 7
- `.cursor/.init-done` 不存在 → 会话首次响应前由 recall 规则自动引导
- 用户关键词:`初始化cursor` / `cursor init` / `重置 cursor` / `reset cursor`
- 口头描述"把复制过来的 .cursor 清理一下"、"按 baserule 归位一下"
### 手动全流程
### B. 仅补 .gitignore只跑阶段 5.5
触发词包括:`初始化 cursor``cursor init``重置 cursor``reset cursor`
“清理复制来的 .cursor”、“按基线归位”。
- 用户关键词:`补 gitignore` / `生成 gitignore` / `gitignore 模板`
- 跳过阶段 1-5 和阶段 7 的除"更新 gitignore_generated 字段"外的其他操作
sentinel 已存在时必须先展示其元数据并再次确认;不得把手动触发当作无提示重置。
## 执行原则(不可协商)
1. **稳定准确 > token 成本**:每一步可以多读、多确认、让用户点头,不要图快
2. **破坏性操作必须 dry-run**:删除、重置必须先列清单给用户确认
3. **不可逆操作前提醒 git**:开始前先提示用户确认工作区已 commit/stash
4. **模糊就问**:分类不清的文件一律问用户,不要猜
5. **幂等**:反复运行应当无害(第二次在已干净状态下不会做破坏)
6. **中断可恢复**:任何阶段异常 abort **不写入** `.init-done`,下次会话仍会被 recall 触发从头继续
### 仅补 `.gitignore`
## 核心流程
触发词包括:`补 gitignore``生成 .gitignore``gitignore 模板`
只执行阶段 5不得改动 `.cursor/` 的其他内容。若 sentinel 存在,只更新
`gitignore_generated`;若不存在,询问是否改跑完整初始化,不得只为补
`.gitignore` 创建 sentinel。
### 阶段 0前置确认 + 幂等判断
## 不可协商的安全契约
```
1. 检查当前工作目录下存在 .cursor/
- 不存在 → 直接结束,提示用户先复制模板
2. 读取 .cursor/.init-done
- 不存在 → 继续,按"首次 init"路径执行
- 已存在 → 展示其元数据initialized_at、project_type 等),提示:
"检测到本仓库已于 {initialized_at} 完成初始化project_type: {type})。
继续执行会清空 deferred / pitfalls / distillery 等数据文件。确认要继续吗?"
等用户明确回复"继续"再往下
3. 运行 `git status`
- 不是 git 仓库 → 警告但不阻塞,建议手动备份
- 有未提交改动 → 提醒"建议先 commit 或 stash便于回滚",等用户明确回复"继续"
4. 向用户声明本次操作范围(见"分组规则"),请求口头确认启动
```
1. **先 dry-run后确认才写入**:删除、重置、移动、覆盖、追加都进入同一份预览清单。
2. **canonical 永不删除**:尤其不得清空 `.cursor/rules/project/`;六个 Godot baseline
project rules 必须保留。
3. **精确白名单**`.cursor/skills/` 最终只能有 reference 中列出的 8 个目录。
4. **不触碰来源树**:只处理目标项目;不得删除或修改 `source/`
5. **幂等**:同一输入重复执行,第二次应只产生空或等价 dry-run。
6. **失败不写 sentinel**:取消、异常、验证失败或未决冲突都不得创建/更新
`.cursor/.init-done`
7. **运行态不冒充 canonical**`.cursor/local-env.json``.cursor/changelog/.changelog-ack`
`.cursor/changelog/.session-start` 都是 gitignored 本机文件,不是 tracked 白名单项。
8. **文本规范**:新写文本使用 UTF-8 无 BOM、LF。
### 阶段 1递归扫描 `.cursor/`
## 阶段 0前置检查
```
1. 用 shell 列出 .cursor 下所有文件(含子目录)
2. 把每个文件对照"规范清单"和"清理清单"分成三组:
- A 组canonical在规范清单中原样保留
- B 组template-data在清理清单中需要删除或重置
- C 组foreign两张清单都没覆盖到的视作目标项目原有文件或未知项
3. 把分组结果用表格汇报给用户:
| 组 | 路径 | 处理动作 |
```
1. 确认项目根存在 `.cursor/`;不存在则停止并说明缺失。
2. 读取 `.cursor/.init-done`
- 不存在:进入首次初始化。
- 存在:展示 `initialized_at``project_type``skill_version`,等待用户明确继续。
3. 执行 `git status`。有未提交改动时建议先 commit/stash并等待明确继续非 Git
仓库则建议备份,但可继续。
4. 读取 [reference.md](reference.md),声明本轮会保留、删除、重置、生成的范围。
5. 未获得明确确认前不得进入写入阶段。
### 阶段 2处理 C 组(目标项目原有文件归位)
## 阶段 1一次性采集本机配置
对 C 组中的每个文件,按 baserule.mdc 的归属约定分类,逐条询问用户(可一次性列清单批量确认)
在**一轮提问**中采集
| 文件特征 | 建议归属 | 目标路径 |
|----------|----------|----------|
| `.mdc` 且内容是跨项目通用规则 | 通用 Rule | `.cursor/rules/common/<name>.mdc` |
| `.mdc` 且内容是当前新项目专属规则 | 项目 Rule | `.cursor/rules/project/<name>.mdc` |
| 包含 `SKILL.md` 的目录 | Skill无论通用/项目专属) | `.cursor/skills/<skill-name>/` |
| 看不出归属 | 询问用户 | — |
| 明显是临时/垃圾文件(`*.log``*.tmp`、缓存等) | 删除(需用户确认) | — |
1. `changelog-author`:稳定、适合文件名的作者 handle。
2. `shell``powershell``bash``zsh`;可把当前探测值作为默认。
3. `godot4_path`Godot 4 executable 的绝对路径;允许留空。
执行要点:
同一轮可附带询问 `.gitignore` 的写入方式(新建 / 追加 / 替换 / 跳过)和自定义忽略项,
不要把三个环境字段拆成多轮追问。仅收集值,此阶段不写文件。
- **先列清单一次性确认**,然后批量执行移动/删除,避免交互过于频繁
- **重名冲突**C 组文件若与 A 组规范文件重名,**保留 A 组版本**,把 C 组备份到 `.cursor/_init-backup/` 下让用户自行 diff不直接覆盖
- **Skill 归属提示**:通用 Skill 建议用户之后手动同步到 `~/.cursor/skills/`(跨项目便利副本,可选)
- **迁移后核查**:移动完成后重新扫描,确认 C 组已清空
### 阶段 3处理 B 组(源项目项目专用数据清理/重置)
按"清理清单"执行:
**A. 直接删除**
- `.cursor/rules/project/*.mdc`(全部)—— 源项目的项目级 rules新项目不适用
- `.cursor/skills/<非规范 skill 目录>/` —— 源项目的项目级 Skills不在白名单的所有 skill 目录)
- `.cursor/plans/` 下所有文件(目录保留,内容清空)
- `.cursor/debug-*.log``.cursor/*.log` —— 调试日志
- `.cursor/local-env.json` —— 设备绑定,需新设备重新生成(`local-env-check.mdc` 自动探测)
**B. 重置为空模板**(保留文件,仅清内容)
| 文件 | 模板(见"模板内容"节) |
|------|------------------------|
| `.cursor/changelog/changelog-full.md` | L1 模板 |
| `.cursor/changelog/changelog-recent.md` | L2 模板 |
| `.cursor/changelog/changelog-headlines.md` | L3 模板 |
| `.cursor/changelog/.changelog-ack` | 空文件touch |
| `.cursor/deferred/registry.md` | deferred 模板 |
| `.cursor/pitfalls/pitfalls.md` | pitfalls 模板 |
| `.cursor/distillery/problems.md` | problems 模板 |
| `.cursor/distillery/insights.md` | insights 模板 |
| `.cursor/distillery/golden-rules.md` | golden-rules 模板 |
| `.cursor/profile/project-profile.md` | project-profile 模板 |
| `.cursor/profile/project-profile-log.md` | project-profile-log 模板 |
执行要点:
- 每个重置前**先 Read 现有内容预览前 10 行**给用户,避免误删有价值数据(特别是 deferred/pitfalls 用户可能想保留)
- 如用户对某个数据文件明确说"保留"(例如 golden-rules 想带过去),跳过该文件的重置
### 阶段 4重建 registry
打开 `.cursor/skills/epee-orchestrator/registry.md`
1. 解析现有条目(每条 `### <skill-name>` 块)
2. 删除所有 `类型: 项目级` 的条目
3. 遍历 `.cursor/skills/` 目录,对照 registry
- registry 有条目但 skill 目录不存在 → 删除条目
- skill 目录存在但 registry 无条目 → 读取该 skill 的 SKILL.md frontmatter新增条目
4. **确保 `cursor-init` 条目本身存在**(类型: 基础设施)
5. 写回 registry.md
### 阶段 5同步 `hooks.json`(跨平台保守处理)
- 读取 `.cursor/hooks.json`
-`command` 字段硬编码为 `powershell ...`,且目标新项目声明/探测到的 shell 不是 powershell参考 `local-env-check.mdc`**提醒用户**需要按 `hooks/README.md` 手动改为 `bash .cursor/hooks/xxx.sh`
- 本阶段不自动改 hooks.json避免误判
### 阶段 5.5`.gitignore` 交互式生成
#### 5.5.1 前置检测
```
检测项目根 .gitignore
不存在 → 进入 5.5.2 问卷
已存在且非空 → Read 前 30 行展示给用户,询问:
[覆盖 / 追加到文件末尾 / 跳过本阶段]
用户选"跳过" → 本阶段结束,.init-done 里标记 gitignore_generated: false
```
#### 5.5.2 问卷(一次性批量 AskQuestion 收集)
必问 3 项 + 可选 1 项:
| 编号 | 问题 | 类型 | 选项 |
|------|------|------|------|
| Q1 | 项目主要技术栈 | 多选 | Unity / Node.js / Python / Rust / Go / C# 非 Unity / Web 静态站点 / 其他 / **不确定(跳过语言段)** |
| Q2 | IDE 偏好 | 多选 | Cursor预勾/ VSCode / JetBrains 全家桶 / Visual Studio / Vim+Emacs |
| Q3 | 操作系统 | 多选 | Windows / macOS / Linux按探测到的当前 OS 预勾) |
| Q4可选| 其他要忽略的路径 | 自由文本 | 用户可跳过 |
#### 5.5.3 片段组装
## 阶段 2扫描、分类与 dry-run
按问卷选择拼装,片段内容见下方"模板内容 · .gitignore 片段库"。拼装顺序固定
递归扫描 `.cursor/`,按 reference 分为
```
# === Cursor固定所有项目都包含 ===
# === OS ===
# === IDE ===
# === Language / Framework ===
# === Custom ===
```
若 Q1 选"不确定" → 跳过 Language 段,其他段正常拼装(极简版仍可用)。
#### 5.5.4 预览与确认
1. 把组装结果写到 `.cursor/_gitignore-preview`(临时文件)
2. 给用户完整展示(超过 60 行则折叠中段)
3. AskQuestion`[写入 .gitignore / 让我调整再确认 / 放弃本阶段]`
4. "写入"→ move 到项目根 `.gitignore`(已存在时按 5.5.1 的选择做覆盖或追加),删除预览文件
5. "调整"→ 根据用户描述修改后重复步骤 2-3
6. "放弃"→ 删除预览文件,`.init-done` 标记 `gitignore_generated: false`
- **A / canonical**:精确白名单,保留;缺失项记为阻塞错误。
- **B / cleanup**:历史 fragment、备份、缓存、设备配置、ack、临时文件和状态数据。
- **C / foreign**:不属于 A/B 的文件。
### 阶段 6验证与报告
处理 C 组:
```
1. 重新递归扫描 .cursor/,再次分组
2. 期望状态:
- A 组:全部保留,文件内容未被动过
- B 组:数据文件仅含模板内容;项目级 rules/skills/日志 已删除
- C 组:为空(或仅剩用户明确要求保留的文件)
- 项目根 .gitignore按用户选择存在或被放弃状态记到报告里
3. 给用户一份结构化报告,至少包含:
- 移动的文件from → to
- 删除的文件
- 重置的文件
- 新注册/移除的 registry 条目
- .gitignore 的处理结果(新建/追加/覆盖/放弃)
- 未处理的文件(如果有,逐条说明原因)
```
- 非白名单 Skill 必须从 `.cursor/skills/` 移除;如用户要保留,先迁出该目录再删原件。
- 非 baseline project rule 视为模板项目遗留;逐项预览,确认后删除或迁出。
- 通用 `.mdc` 可建议迁到 `.cursor/rules/common/`,但只有九个 common rule 属于本基线;
最终仍需用户明确决定。
- 重名冲突永远保留 canonical 版本;冲突副本只能在用户确认后临时放入
`.cursor/_init-backup/`,并须在阶段 6 前移出或删除。
### 阶段 7写入 `.init-done` sentinel
输出一份合并 dry-run至少列出
**仅在阶段 6 验证全部通过后执行**。任何前序阶段异常中断都不写入此文件,确保下次会话 recall 能再次触发。
- 保留及缺失的 canonical 项;
- 删除的文件/目录;
- 重置的状态文件;
- C 组迁移/删除动作;
- `local-env.json` 生成计划;
- registry 重写计划;
- `.gitignore` 新建/追加/替换计划。
```
1. 按"模板内容 · .init-done"格式,填充实际值写入 .cursor/.init-done
2. 向用户输出收尾提示:
> 初始化完成。建议现在执行:
> git add .cursor/ .gitignore
> git diff --cached -- .cursor/
> git commit -m "chore(cursor): initialize .cursor from template"
>
> ⚠️ .cursor/.init-done 必须 commit否则团队其他成员 clone 后会被再次触发 init。
```
等待用户对整份计划明确确认。分类不清就问,不得猜。
**仅跑阶段 5.5 时**(用户说"补 gitignore"
-`.init-done` 存在 → 更新其中的 `gitignore_generated: true` 字段,保留其他字段
-`.init-done` 不存在 → 提示用户"仓库尚未完整 init只补 gitignore 不会写 sentinel。是否改为跑完整 init"
## 阶段 3执行 cleanup
---
严格按已确认 dry-run 和 reference 的 cleanup 表执行:
## 规范清单canonical
1. 删除全部历史 changelog fragments只保留 `entries/.gitkeep`
2. 删除全部 backup、cache、编译缓存、ack、session marker、日志和 init 临时文件。
3. 删除旧 `.cursor/local-env.json`;后续从 example 重新生成。
4. 删除非白名单 Skill最终目录名必须恰好为 8 个。
5. 保留六个 Godot baseline project rules绝不执行“清空 rules/project”。
6. 重置 deferred、pitfalls、distillery、profile 状态文件。
7. 在 entries 为空后运行 `changelog_build.py`,确定性重建四个 changelog 视图。
下列文件构成本 `.cursor` 模板的"主干",初始化后必须都在、内容不被删改(数据文件除外):
某一步失败立即停止;不要继续“尽量完成”,也不要写 sentinel。
### 顶层
## 阶段 4重建本机配置与 Registry
- `.cursor/hooks.json`
- `.cursor/local-env.example.json`
- `.cursor/.init-done` —— **阶段 7 写入**;模板仓库自身不应包含此文件
### `local-env.json`
### Rules通用
1. 读取 `.cursor/local-env.example.json`
2. 深拷贝其 JSON 结构到 `.cursor/local-env.json`,保留 example 中不认识的键。
3. 用阶段 1 的值设置 `changelog-author``shell``godot4_path`
4. `godot4_path` 留空时写空字符串,不伪造路径。
5. 验证 JSON 可解析,且 `.gitignore` 忽略该文件。
- `.cursor/rules/common/baserule.mdc`
- `.cursor/rules/common/changelog-recall.mdc`
- `.cursor/rules/common/deferred-recall.mdc`
- `.cursor/rules/common/distillery-recall.mdc`
- `.cursor/rules/common/epee-orchestrator.mdc`
- `.cursor/rules/common/pitfall-recall.mdc`
- `.cursor/rules/common/profile-recall.mdc`
- `.cursor/rules/common/cursor-init-recall.mdc`
不要引用或等待额外的环境检查 Rule本阶段就是唯一初始化入口。
### Rules项目专属目录
### Skill Registry
- `.cursor/rules/project/`(保留**空目录**;源项目的 `.mdc` 内容一律删除)
`.cursor/skills/epee-orchestrator/registry.md` 校准为磁盘上的 8 个真实 Skill
### Skills规范 Skill 目录白名单)
`cursor-init``deferred-decisions``dev-changelog``epee-orchestrator`
`pitfall-journal``problem-distillery``profile-memory``html-doc`
- `.cursor/skills/cursor-init/`
- `.cursor/skills/deferred-decisions/`
- `.cursor/skills/dev-changelog/`
- `.cursor/skills/epee-orchestrator/`(含 `registry.md`,见阶段 4
- `.cursor/skills/pitfall-journal/`
- `.cursor/skills/problem-distillery/`
- `.cursor/skills/profile-memory/`
不得保留这 8 项之外的条目或任何磁盘上不存在的 Skill。
每条的路径、类型、能力、触发场景、输出必须与对应 `SKILL.md` 一致。
> **任何不在此白名单中的 skill 目录**,在阶段 3 中一律删除(视为源项目的项目级 Skill
## 阶段 5生成或补齐 `.gitignore`
### Hooks
使用 [reference.md](reference.md) 的片段,不得临时拼写:
- `.cursor/hooks/README.md`
- `.cursor/hooks/run-hook.ps1`
- `.cursor/hooks/session-init.ps1`
- `.cursor/hooks/session-init.sh`
- `.cursor/hooks/check-changelog.ps1`
- `.cursor/hooks/check-changelog.sh`
1. 固定加入 Cursor 运行态片段。
2. 固定加入 Godot 片段:忽略 `.godot/`,但提交 `*.uid` 和资源 `*.import` sidecar。
3. 按用户 shell/OS 与自定义项补充其他片段。
4. 已有 `.gitignore` 时按阶段 1 的新建/追加/替换/跳过选择处理。
5. 写入前展示最终预览;用户调整后重新预览。
6. 清除与 `*.uid``*.import` 提交要求冲突的旧忽略规则;不要忽略
`.cursor/.init-done`
### 数据占位目录(保留目录,文件重置为模板内容)
用户跳过时记录 `gitignore_generated: false`;这不等于流程失败,但报告中必须明确。
- `.cursor/changelog/{changelog-full.md, changelog-recent.md, changelog-headlines.md, .changelog-ack}`
- `.cursor/deferred/registry.md`
- `.cursor/pitfalls/pitfalls.md`
- `.cursor/distillery/{problems.md, insights.md, golden-rules.md}`
- `.cursor/profile/{project-profile.md, project-profile-log.md}`
- `.cursor/plans/`(空目录)
## 阶段 6验证
---
逐项验证,任何失败都回到修复或停止状态:
## 清理清单cleanup list
1. reference 中全部 canonical 文件存在。
2. 九个 common rules 和六个 Godot baseline project rules 全部存在。
3. `.cursor/skills/` 的一级目录集合与 8 项白名单**完全相等**。
4. 所有历史 fragments、backup、cache、旧 ack、session marker 和临时文件均不存在。
5. `entries/.gitkeep` 存在entries 下无其他文件。
6. 四个 changelog 视图与空 entries 经生成器重建的结果一致。
7. 状态文件等于 reference 模板。
8. `local-env.json` 可解析、字段与用户输入一致且被 Git 忽略。
9. registry 恰有 8 项,路径均存在,无三个排除项或旧项目名。
10. 所有 Skill frontmatter `name` 与目录一致description 同时说明 what + when。
11. Skill 内相对引用均存在;不存在环境检查 Rule 的悬空引用。
12. `.gitignore` 忽略 `.godot/``local-env.json`、ack、session marker同时不忽略 `*.uid`
资源 `*.import`
13. 本次写入的文本为 UTF-8 无 BOM、LF。
### 强制删除
报告移动、删除、重置、生成、跳过和未解决项。未解决项不为空时不得进入阶段 7。
| 路径 | 原因 |
|------|------|
| `.cursor/rules/project/*.mdc` | 源项目的项目级 Rules |
| `.cursor/skills/<非白名单>/` | 源项目的项目级 Skills |
| `.cursor/plans/*`(内容) | 源项目规划文档 |
| `.cursor/*.log``.cursor/debug-*.log` | 调试日志遗留 |
| `.cursor/local-env.json` | 设备绑定配置,需由新设备重新生成 |
| `.cursor/_init-backup/`(如果是上一轮残留) | 仅阶段 2 冲突备份用,运行前应不存在 |
| `.cursor/_gitignore-preview`(如果是上一轮残留) | 阶段 5.5 临时文件 |
## 阶段 7写 sentinel
### 重置为模板
仅在阶段 6 全部通过后,按 reference 模板写 `.cursor/.init-done`。写入后再次读取并验证
字段。`project_type` 固定为 `godot``gitignore_generated` 反映阶段 5 实际结果。
见"模板内容"节
最后提醒用户检查 `git status` 和 diff并提交 `.cursor/.init-done`;不要代替用户提交
---
## 验收结果
## 模板内容
完整初始化后:
重置数据文件时使用以下内容。带 `{{DATE}}` 的占位符替换为当天日期(`YYYY-MM-DD`)。
### changelog-full.md
```markdown
# Dev Changelog — Full
完整的开发改动记录,按时间倒序排列。作为主动 RAG 的数据源,用户手动唤醒时读取。
## 记录
```
### changelog-recent.md
```markdown
# Dev Changelog — Recent
最近 ~10 次改动的摘要记录,按时间倒序排列。
当 Agent 检测到当前任务与近期改动相关时自动读取。
```
### changelog-headlines.md
```markdown
# Dev Changelog — Headlines
最近 ~50 次改动的一句话概要,按时间倒序排列。每次会话自动注入上下文。
```
### .changelog-ack
空文件0 字节),只用于更新 mtime 阻止 hook 重复提醒。
### deferred/registry.md
```markdown
# Deferred Decisions Registry
## Active Items
(暂无延期方案)
---
## Completed / Cancelled Items
(暂无已完成或已废弃的方案)
```
### pitfalls/pitfalls.md
```markdown
# Pitfall Journal
开发过程中踩过的坑,按时间倒序排列。
Agent 进入 Debug mode 或遇到运行时错误时自动检索匹配。
---
```
### distillery/problems.md
```markdown
# Problem Distillery — Problems
反复出现的顽固问题追踪记录,按时间倒序排列。
<!-- 新条目追加在此行下方 -->
```
### distillery/insights.md
```markdown
# Problem Distillery — Insights
从已解决的顽固问题中蒸馏出的精炼方法论。
<!-- 新条目追加在此行下方 -->
---
last_distill_date: {{DATE}}
```
### distillery/golden-rules.md
```markdown
# Golden Rules
经过实践验证(权重 >= 5的精炼认知每次新会话自动注入。
<!-- 当条目达到权重阈值后由 Agent 自动写入 -->
```
### profile/project-profile.md
```markdown
# Project Profile
## 项目定位
## 技术栈与架构
## 设计约定
## 产品方向
```
### profile/project-profile-log.md
```markdown
# Project Profile Log
详细记录每次项目画像更新的完整上下文,按时间正序追加。
## 记录
```
### .init-done
阶段 7 写入的 sentinelYAML 格式,字段固定:
```yaml
# .cursor/.init-done — cursor-init Skill 写入的初始化标记文件
# 本文件的存在表示本仓库已完成 .cursor 模板初始化
# 请务必 git commit 此文件,避免团队成员 clone 后被再次触发 init
initialized_at: {{ISO_DATETIME}} # 如 2026-04-21T16:30:00+08:00
initialized_by: cursor-init
skill_version: 2
project_type: {{PROJECT_TYPE}} # 阶段 5.5 问卷 Q1 结果;多选逗号分隔;"不确定"写 unknown
gitignore_generated: {{BOOL}} # true / false
```
**写入时机**
- 全流程跑完(阶段 6 验证通过)时由阶段 7 写入
- "补 gitignore" 单独跑阶段 5.5 时,若已存在则只更新 `gitignore_generated` 字段
---
## 模板内容 · .gitignore 片段库
阶段 5.5 按用户问卷选择拼接以下片段。每段前后各空一行,保证可读性。
### 固定段Cursor
```gitignore
# === Cursor ===
# 设备绑定配置,不同机器需独立生成
.cursor/local-env.json
# Hook 运行态标记(仅影响本地会话)
.cursor/changelog/.changelog-ack
# cursor-init 临时产物
.cursor/_init-backup/
.cursor/_gitignore-preview
```
### OS 段(按问卷 Q3 多选拼接)
**Windows**
```gitignore
# === OS: Windows ===
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
Desktop.ini
$RECYCLE.BIN/
*.stackdump
*.lnk
```
**macOS**
```gitignore
# === OS: macOS ===
.DS_Store
.AppleDouble
.LSOverride
Icon
._*
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
```
**Linux**
```gitignore
# === OS: Linux ===
*~
.fuse_hidden*
.directory
.Trash-*
.nfs*
```
### IDE 段(按问卷 Q2 多选拼接)
**VSCode**
```gitignore
# === IDE: VSCode ===
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
.history/
*.vsix
```
**JetBrains**
```gitignore
# === IDE: JetBrains ===
.idea/
*.iml
*.ipr
*.iws
.idea_modules/
atlassian-ide-plugin.xml
```
**Visual Studio**
```gitignore
# === IDE: Visual Studio ===
.vs/
*.user
*.suo
*.userprefs
bin/
obj/
[Dd]ebug/
[Rr]elease/
x64/
x86/
```
**Vim/Emacs**
```gitignore
# === IDE: Vim / Emacs ===
*.swp
*.swo
*.swn
Session.vim
.netrwhist
*~
\#*\#
.\#*
```
**Cursor**:固定段已覆盖,无需额外片段。
### Language / Framework 段(按问卷 Q1 多选拼接)
**Unity**
```gitignore
# === Language: Unity ===
[Ll]ibrary/
[Tt]emp/
[Oo]bj/
[Bb]uild/
[Bb]uilds/
[Ll]ogs/
[Uu]ser[Ss]ettings/
[Mm]emoryCaptures/
[Rr]ecordings/
sysinfo.txt
*.apk
*.aab
*.unitypackage
*.app
*.csproj
*.unityproj
*.sln
*.suo
*.tmp
*.user
*.userprefs
*.pidb
*.booproj
*.svd
*.pdb
*.mdb
*.opendb
*.VC.db
```
**Node.js**
```gitignore
# === Language: Node.js ===
node_modules/
.npm/
.yarn/
.pnp.*
dist/
build/
out/
.next/
.nuxt/
.cache/
.parcel-cache/
coverage/
.env
.env.local
.env.*.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
.turbo/
```
**Python**
```gitignore
# === Language: Python ===
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
dist/
*.egg-info/
*.egg
.venv/
venv/
env/
.pytest_cache/
.mypy_cache/
.ruff_cache/
.coverage
.coverage.*
htmlcov/
.tox/
.nox/
.hypothesis/
.ipynb_checkpoints/
```
**Rust**
```gitignore
# === Language: Rust ===
target/
**/*.rs.bk
*.pdb
# Cargo.locklib crate 建议忽略bin crate 建议提交,默认不忽略
# Cargo.lock
```
**Go**
```gitignore
# === Language: Go ===
bin/
vendor/
*.exe
*.exe~
*.dll
*.so
*.dylib
*.test
*.out
go.work
```
**C# 非 Unity**
```gitignore
# === Language: C# (non-Unity) ===
bin/
obj/
*.user
*.suo
*.pdb
*.cache
[Dd]ebug/
[Rr]elease/
x64/
x86/
[Bb]uild/
*.dll
*.pdb
```
**Web 静态站点**
```gitignore
# === Language: Web Static ===
node_modules/
dist/
build/
public/build/
.cache/
.tmp/
.sass-cache/
.parcel-cache/
```
**其他**:不拼接语言段,用户在 `# === Custom ===` 段自行补充。
### Custom 段(固定尾部)
```gitignore
# === Custom ===
# 在此段追加项目专属需要忽略的路径
```
若用户在问卷 Q4 填了自由文本,按行拆分后追加到 Custom 段下方。
---
## 冲突与边界情况处理
| 场景 | 处理 |
|------|------|
| C 组某文件与 A 组重名 | 保留 A 组C 组移到 `.cursor/_init-backup/`,告知用户自行 diff |
| C 组有 `SKILL.md` 且 skill 名与 A 组白名单同名 | 保留 A 组 skillC 组整个 skill 目录移到 `_init-backup/` |
| 数据文件(如 `pitfalls.md`)用户明确说"保留" | 跳过该文件重置 |
| `local-env.json` 存在且用户说"我就在这台机器上开 B保留它" | 跳过删除,但提醒其已在 `.gitignore` |
| `.cursor/rules/project/` 为空目录 | 保留空目录(便于未来放项目级 rules |
| 扫描到 `.cursor/_init-backup/`(上轮残留) | 警告用户并询问:删除 / 保留 / 重命名 |
| `git status` 不能执行(不是 git 仓库) | 警告但不阻塞,改为建议用户手动备份 |
| `.cursor/.init-done` 已存在且用户触发了全流程 | 阶段 0 展示其元数据并要求二次确认 |
| 阶段 5.5 项目根已有 `.gitignore` 且用户选"追加" | 把拼装结果追加到现有文件末尾(用 `\n\n# --- 以下由 cursor-init 追加 ---\n` 分隔) |
| 阶段 5.5 中途异常或用户放弃 | 不写入 `.gitignore``.init-done``gitignore_generated` 记 false |
| 阶段 7 写 `.init-done` 前前序阶段已 abort | 不写入 sentinel下次会话 recall 会重新触发 |
| 用户说"补 gitignore" 但 `.init-done` 不存在 | 提醒"仓库尚未完整 init",询问是否改为跑全流程 |
---
## 与其他系统的协作
| 系统 | 关系 | 说明 |
|------|------|------|
| `baserule.mdc` | 分类权威 | 阶段 2 归位目录严格按 baserule 约定 |
| `cursor-init-recall.mdc` | 触发器 | 负责首次自动触发alwaysApply + sentinel 检查)和关键词触发 |
| `epee-orchestrator` Skill | 双向 | 本 Skill 被 Orchestrator 在 registry 里注册;同时本 Skill 负责清理 registry |
| `dev-changelog` / `pitfall-journal` / `deferred-decisions` / `problem-distillery` / `profile-memory` | 下游数据主人 | 本 Skill 只在阶段 3 重置其数据文件,不改动 SKILL.md |
| `local-env-check.mdc` | 后继 | 本 Skill 删除 `local-env.json`,后续新会话由 `local-env-check` 自动重新探测并生成 |
---
## 一次运行的最终状态(验收标准)
全流程(阶段 0 → 7完成后 `.cursor/` 应当满足:
1. 规范清单中列出的所有文件/目录都存在
2. 所有数据文件changelog/deferred/pitfalls/distillery/profile仅含模板内容
3. `.cursor/rules/project/` 为空目录
4. `.cursor/skills/` 下仅有白名单中的 skill 目录
5. `.cursor/skills/epee-orchestrator/registry.md` 中只有:基础设施 + 个人级 skill 条目,且包含 `cursor-init` 条目
6. 不存在 `*.log``local-env.json``_init-backup/``_gitignore-preview`
7. `.cursor/.init-done` 已写入,字段完整且值有效
8. 项目根 `.gitignore` 状态明确(存在且有内容 / 被用户显式放弃)
9. `git status .cursor/ .gitignore` 能让用户清楚看到所有改动
仅跑阶段 5.5"补 gitignore")完成后:
1. 项目根 `.gitignore` 存在或被显式放弃
2.`.init-done` 已存在,其 `gitignore_generated` 字段被更新
3. 其他 `.cursor/` 内容**零改动**
---
## 自迭代日志
本节记录使用本 Skill 过程中发现的必要检查项。
### 已知必要检查
1. **阶段 2 的 C 组归位前必须先做 dry-run 展示** —— 批量移动 .mdc / SKILL 文件是高破坏性操作,只要有一条分类错误就会污染 canonical 结构,必须让用户在列表上逐条过一遍再执行。
2. **重名冲突不许直接覆盖** —— C 组与 A 组重名时A 组(模板版本)永远是 source of truth冲突文件只能进 `_init-backup/`,让用户自行决定是否把差异合并回 common rule。
3. **registry 重建时先读后写** —— 阶段 4 不要直接用硬编码模板覆盖 registry.md必须先解析现有条目只移除项目级条目、补齐缺失条目避免丢失用户自己添加的个人级 Skill 条目。
4. **`.init-done` 是完成度契约** —— 只有阶段 6 验证通过后才能写入此文件。中途 abort 必须让 sentinel 缺失,这是保证下次会话能恢复触发的关键设计。
5. **`.gitignore` 用户主导** —— 阶段 5.5 对语言栈/IDE/OS 的判断完全来自用户问卷,不要根据文件探测"智能推断",因为新项目此时通常还是空白,推断不准反而会填错条目。
- baseline 结构完整,六个 Godot project rules 仍在;
- `.cursor/skills/` 只有指定 8 个目录;
- 历史数据、backup、cache、ack 与临时文件已清;
- 状态模板与四视图已重置;
- `local-env.json` 已由 example 生成且 gitignored
- `.gitignore` 正确区分 Godot 缓存和需提交的 sidecar
- `.init-done` 只在所有验证通过后存在。

View File

@@ -0,0 +1,552 @@
# Cursor Init Reference
本文件是 `cursor-init` 的精确数据契约。`SKILL.md` 负责流程,本文件负责 canonical、
cleanup、状态模板、sentinel 与 `.gitignore` 片段。路径均相对项目根。
## 1. Canonical 白名单
canonical 是初始化期间必须保留的 tracked 基线。缺失项应报错并从可信模板恢复,
不得用空文件伪造。除这里列出的 8 个 Skill 目录外,`.cursor/skills/` 不允许有其他
一级目录。
### 1.1 顶层配置与生成标记
```text
.cursor/hooks.json
.cursor/local-env.example.json
.cursor/.init-done
```
说明:
- `.cursor/.init-done` 是阶段 7 生成后需要提交的文件;模板源在首次 init 前可以没有。
- `.cursor/local-env.json` 是由 example 生成的 gitignored 运行态,不是 canonical。
- `.cursor/changelog/.changelog-ack` 是 gitignored hook 运行态,不是 canonical。
- `.cursor/changelog/.session-start` 是 sessionStart hook 刷新的 gitignored 运行态,不是 canonical。
### 1.2 Common Rules恰好 9 个基线项)
```text
.cursor/rules/common/baserule.mdc
.cursor/rules/common/changelog-recall.mdc
.cursor/rules/common/cursor-init-recall.mdc
.cursor/rules/common/deferred-recall.mdc
.cursor/rules/common/distillery-recall.mdc
.cursor/rules/common/epee-orchestrator.mdc
.cursor/rules/common/pitfall-recall.mdc
.cursor/rules/common/profile-recall.mdc
.cursor/rules/common/dependency-governance.mdc
```
### 1.3 Godot baseline Project Rules首次 init 必须保留)
```text
.cursor/rules/project/godot-architecture.mdc
.cursor/rules/project/godot-assets.mdc
.cursor/rules/project/godot-data-systems.mdc
.cursor/rules/project/godot-gdscript-style.mdc
.cursor/rules/project/godot-scene-conventions.mdc
.cursor/rules/project/godot-testing.mdc
```
禁止用 `.cursor/rules/project/*.mdc` 之类的通配删除覆盖这些文件。
### 1.4 Skills最终恰好 8 个目录)
```text
.cursor/skills/cursor-init/
.cursor/skills/deferred-decisions/
.cursor/skills/dev-changelog/
.cursor/skills/epee-orchestrator/
.cursor/skills/pitfall-journal/
.cursor/skills/problem-distillery/
.cursor/skills/profile-memory/
.cursor/skills/html-doc/
```
目录内 canonical 文件:
```text
.cursor/skills/cursor-init/SKILL.md
.cursor/skills/cursor-init/reference.md
.cursor/skills/deferred-decisions/SKILL.md
.cursor/skills/dev-changelog/SKILL.md
.cursor/skills/epee-orchestrator/SKILL.md
.cursor/skills/epee-orchestrator/registry.md
.cursor/skills/pitfall-journal/SKILL.md
.cursor/skills/problem-distillery/SKILL.md
.cursor/skills/profile-memory/SKILL.md
.cursor/skills/html-doc/SKILL.md
.cursor/skills/html-doc/assets/theme.css
.cursor/skills/html-doc/assets/annotate.js
.cursor/skills/html-doc/templates/skeleton.html
```
### 1.5 Hooks 全套
```text
.cursor/hooks.json
.cursor/hooks/README.md
.cursor/hooks/run-hook.ps1
.cursor/hooks/session-init.ps1
.cursor/hooks/session-init.sh
.cursor/hooks/check-changelog.ps1
.cursor/hooks/check-changelog.sh
```
### 1.6 Changelog tools 全套
```text
.cursor/changelog/tools/changelog_build.py
.cursor/changelog/tools/migrate_changelog.py
.cursor/changelog/tools/install-git-hooks.ps1
.cursor/changelog/tools/install-git-hooks.sh
.cursor/changelog/tools/merge-import.ps1
.cursor/changelog/tools/merge-import.sh
.cursor/changelog/tools/git-hooks/pre-commit
.cursor/changelog/tools/git-hooks/post-merge
```
`tools/__pycache__/``*.pyc` 永远不是 canonical。
### 1.7 Changelog 四视图与 entries 占位
```text
.cursor/changelog/changelog-full.md
.cursor/changelog/changelog-recent.md
.cursor/changelog/changelog-headlines.md
.cursor/changelog/changelog-by-author.md
.cursor/changelog/entries/.gitkeep
```
四视图由 `changelog_build.py` 生成,不手改。初始化时先清空 entries保留
`.gitkeep`),再运行生成器得到空视图。
### 1.8 状态模板文件
```text
.cursor/deferred/registry.md
.cursor/pitfalls/pitfalls.md
.cursor/distillery/problems.md
.cursor/distillery/insights.md
.cursor/distillery/golden-rules.md
.cursor/profile/project-profile.md
.cursor/profile/project-profile-log.md
```
## 2. Cleanup 清单
### 2.1 强制删除
下面内容必须进入 dry-run确认后删除
```text
# 全部历史 changelog fragment唯一例外是 entries/.gitkeep
.cursor/changelog/entries/**
!.cursor/changelog/entries/.gitkeep
# 历史备份
.cursor/changelog/_backup/**
.cursor/_init-backup/**
.cursor/**/_backup/**
.cursor/**/*.bak
.cursor/**/*.backup
# 缓存和编译产物
.cursor/**/__pycache__/**
.cursor/**/*.pyc
.cursor/**/*.pyo
.cursor/**/*.cache
.cursor/**/.cache/**
# 设备绑定与 hook 运行态
.cursor/local-env.json
.cursor/changelog/.changelog-ack
.cursor/changelog/.session-start
# init / 调试临时产物
.cursor/_gitignore-preview
.cursor/**/*.tmp
.cursor/**/*.temp
.cursor/**/*.log
.cursor/debug-*.log
```
模式中的 `!` 只是“例外”说明,不表示可把这段直接交给任意删除命令。实现时必须先枚举
实际文件并逐项比对白名单,防止通配误删。
此外:
- 删除 `.cursor/skills/` 下所有非 8 项白名单目录。
- 删除或迁出非 baseline project rules但必须逐项确认六个 baseline 永不删除。
- 删除任何非 8 项白名单 Skill。
- 不删除 `source/`,不修改项目根 Git 文件(`.gitignore` 仅在用户明确选择时例外)。
### 2.2 重置而非删除
以下文件保留路径并重置为第 3 节模板:
```text
.cursor/deferred/registry.md
.cursor/pitfalls/pitfalls.md
.cursor/distillery/problems.md
.cursor/distillery/insights.md
.cursor/distillery/golden-rules.md
.cursor/profile/project-profile.md
.cursor/profile/project-profile-log.md
```
四个 changelog 视图不使用手写模板覆盖:删除 fragments 后运行生成器重建。
### 2.3 环境配置入口
本机环境只由 `cursor-init` 阶段 1 和阶段 4 一次性采集、生成,不依赖额外环境检查 Rule。
## 3. 状态模板
写入时替换 `{{DATE}}` 为本地日期 `YYYY-MM-DD`
### 3.1 `.cursor/deferred/registry.md`
```markdown
# Deferred Decisions Registry
## Active Items
(暂无延期方案)
---
## Completed / Cancelled Items
(暂无已完成或已废弃的方案)
```
### 3.2 `.cursor/pitfalls/pitfalls.md`
```markdown
# Pitfall Journal
开发过程中踩过的坑,按时间倒序排列。
Agent 进入 Debug mode 或遇到运行时错误时自动检索匹配。
---
```
### 3.3 `.cursor/distillery/problems.md`
```markdown
# Problem Distillery — Problems
反复出现的顽固问题追踪记录,按时间倒序排列。
<!-- 新条目追加在此行下方 -->
```
### 3.4 `.cursor/distillery/insights.md`
```markdown
# Problem Distillery — Insights
从已解决的顽固问题中蒸馏出的精炼方法论。
<!-- 新条目追加在此行下方 -->
---
last_distill_date: {{DATE}}
```
### 3.5 `.cursor/distillery/golden-rules.md`
```markdown
# Golden Rules
经过实践验证(权重 >= 5的精炼认知每次新会话自动注入。
<!-- 当条目达到权重阈值后由 Agent 自动写入 -->
```
### 3.6 `.cursor/profile/project-profile.md`
```markdown
# Project Profile
## 项目定位
## 技术栈与架构
## 设计约定
## 产品方向
```
### 3.7 `.cursor/profile/project-profile-log.md`
```markdown
# Project Profile Log
详细记录每次项目画像更新的完整上下文,按时间正序追加。
## 记录
```
## 4. 空 Changelog 四视图
`.cursor/changelog/tools/changelog_build.py` 的输出为准。entries 仅有 `.gitkeep` 时,
生成结果应具备以下固定头部且无条目:
### 4.1 `changelog-headlines.md`
```markdown
# Dev Changelog — Headlines
最近 ~50 次改动的一句话概要,按时间倒序排列。每次会话自动注入上下文。
> 本文件由 `tools/changelog_build.py` 从 `entries/` 自动生成,请勿手改。
```
### 4.2 `changelog-recent.md`
```markdown
# Dev Changelog — Recent
最近 ~20 次改动的摘要记录,按时间倒序排列。
当 Agent 检测到当前任务与近期改动相关时自动读取。
> 本文件由 `tools/changelog_build.py` 从 `entries/` 自动生成,请勿手改。
```
### 4.3 `changelog-full.md`
```markdown
# Dev Changelog — Full
完整的开发改动记录,按时间倒序排列。作为主动 RAG 的数据源,用户手动唤醒时读取。
> 本文件由 `tools/changelog_build.py` 从 `entries/` 自动生成,请勿手改;新增/修改请编辑 `entries/<author>/<id>.md` 后重跑生成器。
## 记录
```
### 4.4 `changelog-by-author.md`
```markdown
# Dev Changelog — By Author
按作者聚合的贡献概览,便于多人协作时区分提交来源。
> 本文件由 `tools/changelog_build.py` 自动生成,请勿手改。
```
生成器统一以单个 LF 结尾,不保留多余空白行;验证应比较生成器实际输出,不自行 trim 后回写。
## 5. `local-env.json` 生成契约
必须读取 `.cursor/local-env.example.json`,复制完整 JSON 结构,再覆盖三个键:
```json
{
"shell": "{{SHELL}}",
"changelog-author": "{{CHANGELOG_AUTHOR}}",
"godot4_path": "{{GODOT4_EXECUTABLE_OR_EMPTY}}"
}
```
规则:
- example 中其他键原样保留,不以这段最小示例覆盖整个文件。
- `godot4_path` 可为空字符串。
- 不写用户未确认的自动猜测值。
- 文件必须能被 JSON parser 读取,并由 `.gitignore` 忽略。
- 不提交 `local-env.json`
## 6. `.init-done` 模板
仅在全部验证通过后写入:
```yaml
# .cursor/.init-done — cursor-init 写入的初始化标记
# 必须提交此文件,避免 clone 后重复触发初始化
initialized_at: {{ISO_DATETIME}}
initialized_by: cursor-init
skill_version: 3
project_type: godot
gitignore_generated: {{BOOL}}
```
`{{BOOL}}` 只能是 `true``false`。流程失败、取消或存在未解决项时不得写入。
## 7. `.gitignore` 片段
拼装顺序:
1. Cursor runtime
2. Godot
3. OS / IDE / Language如用户需要
4. Custom
追加到已有文件时,用以下分隔:
```gitignore
# --- 以下由 cursor-init 追加 ---
```
### 7.1 Cursor runtime固定
```gitignore
# === Cursor runtime ===
.cursor/local-env.json
.cursor/changelog/.changelog-ack
.cursor/changelog/.session-start
.cursor/_init-backup/
.cursor/_gitignore-preview
# .cursor/.init-done 是需提交的完成标记,不得忽略
```
### 7.2 Godot固定
```gitignore
# === Godot ===
# 编辑器导入缓存可重建,不提交
.godot/
# Godot 4 UID sidecar 与资源导入配置是项目元数据,必须提交
!*.uid
!**/*.uid
!*.import
!**/*.import
```
写入前检查并移除或修正已有的冲突规则,例如 `*.uid``*.import``**/*.uid`
`**/*.import`。不要把 `.godot/` 内的生成缓存误认为资源旁的 `*.import` sidecar。
### 7.3 Windows
```gitignore
# === OS: Windows ===
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
Desktop.ini
$RECYCLE.BIN/
*.stackdump
*.lnk
```
### 7.4 macOS
```gitignore
# === OS: macOS ===
.DS_Store
.AppleDouble
.LSOverride
Icon
._*
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
```
### 7.5 Linux
```gitignore
# === OS: Linux ===
*~
.fuse_hidden*
.directory
.Trash-*
.nfs*
```
### 7.6 VS Code
```gitignore
# === IDE: VS Code ===
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
.history/
*.vsix
```
### 7.7 JetBrains
```gitignore
# === IDE: JetBrains ===
.idea/
*.iml
*.ipr
*.iws
.idea_modules/
```
### 7.8 Visual Studio
```gitignore
# === IDE: Visual Studio ===
.vs/
*.user
*.suo
*.userprefs
bin/
obj/
[Dd]ebug/
[Rr]elease/
x64/
x86/
```
### 7.9 Vim / Emacs
```gitignore
# === IDE: Vim / Emacs ===
*.swp
*.swo
*.swn
Session.vim
.netrwhist
*~
\#*\#
.\#*
```
### 7.10 Custom
```gitignore
# === Custom ===
# 在此追加项目专属忽略项
```
## 8. Dry-run 输出格式
```markdown
## cursor-init dry-run
### 保留
- path — canonical
### 缺失(阻塞)
- path — 需要从可信模板恢复
### 删除
- path — cleanup 原因
### 重置
- path — 使用的模板
### 迁移 / 冲突
- from → to — 原因与是否需要用户选择
### 生成
- .cursor/local-env.json — 输入字段摘要(不要泄露不必要的本机信息)
- .cursor/skills/epee-orchestrator/registry.md — 8 项
- .gitignore — 新建 / 追加 / 替换 / 跳过
- .cursor/.init-done — 仅验证成功后
```
确认必须覆盖整份 dry-run。dry-run 后扫描结果变化时应重新生成清单并再次确认。

View File

@@ -1,259 +1,222 @@
---
name: dev-changelog
description: >-
三层开发进程记录系统。在 Agent 完成代码改动后自动记录,提供从一句话概要到完整日志的
多级上下文,帮助 Agent 在跨会话场景下保持对项目开发进展的感知。
三层开发进程记录系统fragment 源 + 自动生成视图。Agent 完成代码改动后,
写入一条独立的 fragment 文件并重建视图,提供从一句话概要到完整日志的多级上下文,
帮助 Agent 跨会话保持对项目进展的感知,并支持多人协作下的低冲突合并与作者区分。
当用户提到"开发日志"、"changelog"、"最近改了什么"、"回顾改动"等时触发。
---
# Dev Changelog
三层开发进程记录系统,解决 Agent 跨会话的上下文断裂问题。
跨会话开发进程记录系统。**数据源是一条一文件的 fragment三层视图由脚本自动生成。**
这套设计同时解决了三个老问题:体量失控、三层手动同步漂移、以及多人协作时 git merge 冲突。
## 三层架构
## 架构总览
| 层级 | 文件 | 信息密度 | 条目数量 | 注入方式 |
|------|------|---------|---------|---------|
| L1 完整版 | `changelog-full.md` | 高5-15 行/条) | 无上限,只追加 | 用户手动唤醒 |
| L2 中期版 | `changelog-recent.md` | 中3-5 行/条) | 滚动窗口 ~20 条 | 检测到关联时自动读取 |
| L3 概要版 | `changelog-headlines.md` | 低1 行/条) | 滚动窗口 ~50 条 | 每次会话自动注入 |
```
.cursor/changelog/
├─ entries/<author>/<id>.md ← 唯一数据源(一条改动 = 一个文件,提交进 git
├─ changelog-full.md ← 视图L1 完整,全部条目) \
├─ changelog-recent.md ← 视图L2 摘要,最近 ~20 条) | 全部由
├─ changelog-headlines.md ← 视图L3 一句话,最近 ~50 条) | 生成器产出
├─ changelog-by-author.md ← 视图(按作者聚合) /
└─ tools/
├─ changelog_build.py ← 生成器entries/ → 4 个视图确定性、stdlib-only
├─ migrate_changelog.py ← 一次性迁移脚本(已执行,留作参考)
├─ install-git-hooks.ps1/.sh ← 安装 pre-commit / post-merge git hook
└─ git-hooks/{pre-commit,post-merge}
```
所有数据文件存放在 `.cursor/changelog/` 目录下。
| 层级 | 视图文件 | 信息密度 | 窗口 | 注入方式 |
|------|---------|---------|------|---------|
| L1 完整版 | `changelog-full.md` | 高(完整正文) | 全部 | 用户手动唤醒 |
| L2 中期版 | `changelog-recent.md` | 中3-5 行摘要) | ~20 条 | 检测到关联时自动读取 |
| L3 概要版 | `changelog-headlines.md` | 低1 行) | ~50 条 | 每次会话自动注入 |
| 作者视图 | `changelog-by-author.md` | 索引 | 全部 | 按需 |
> **重要**:视图文件顶部都标了"由 changelog_build.py 自动生成,请勿手改"。
> 一切修改都改 fragment再跑生成器。视图改了不算数下次生成会被覆盖
## 锚点 ID 机制
每条记录在写入时生成一个**锚点 ID**格式 `CL-YYYYMMDD-HHMM`(如 `CL-20260412-1430`)。
同一分钟内有多条时追加字母后缀`CL-20260412-1430a``CL-20260412-1430b`)。
格式 `CL-YYYYMMDD-HHMM-<author>`(如 `CL-20260715-1521-alice`)。同一分钟多条时在 `HHMM`
追加字母(`CL-...-1521a-sq`)。历史迁移条目无作者后缀(`author: legacy`),保持原 ID 不变
以维持 pitfalls / deferred / source_chat 等处的交叉引用有效。
锚点 ID 在三层文件中保持一致,用于跨层精准定位:
- L3 一句话条目以 `[CL-xxx]` 开头
- L2 摘要条目的 H3 标题包含 `[CL-xxx]`
- L1 完整条目的 H3 标题包含 `[CL-xxx]`
锚点 ID 在 fragment 与三层视图中一致,用于逐级 Grep 精准定位L3 → L2 → L1
这使得从 L3 → L2 → L1 的逐级查找可以通过 Grep 精准定位,无需全文读取。
## 作者标识(多人协作核心)
## 逐级上溯机制
每条 fragment 记录作者,体现在三处:① 目录 `entries/<author>/`、② 文件名 / ID 后缀、
③ frontmatter `author` 字段。视图层 L3 行前缀 `@<author>`、L2/L1 有 `author` 字段、
另有按作者聚合的 `changelog-by-author.md`
当 Agent 在使用 L3 概要作为上下文时,如果某条记录的一句话描述**语义模糊**
(如无法判断改动的具体范围、与当前任务的关系不明确),执行以下逐级查找:
**作者 handle 的确定顺序**(写入时):
1.`.cursor/local-env.json``changelog-author` 字段设备绑定、gitignored每位开发者各自配一次最稳
2. 缺失则取 `git config user.name`,转小写、取首个英文单词或姓名缩写作为 handle
3. 仍无法确定则用 `unknown`
```
1. 从 L3 条目中提取锚点 ID如 CL-20260412-1430
2. 在 changelog-recent.mdL2中 Grep 该 ID
- 找到 → 读取该条目的 3-5 行摘要,通常足以消除歧义
- 未找到(已滚出 L2 窗口)→ 进入步骤 3
3. 在 changelog-full.mdL1中 Grep 该 ID
- 找到 → 用 Read 工具读取该 ID 所在行号 ±20 行范围(精准读取,不读全文)
- 未找到 → 放弃上溯,该条目上下文不可用
> 建议每位协作者在本机 `.cursor/local-env.json` 里加 `"changelog-author": "<你的handle>"`
> 保证 handle 稳定(不随 git user.name 大小写/格式变化而漂移)。
## fragment 文件格式
```markdown
---
id: CL-20260715-1521-alice
date: 2026-07-15 15:21
author: alice
author_name: Alice
type: fix
merge_source: HEAD
status: active
superseded_by:
title: 一句话标题(用于视图的 H3 标题)
source_chat: [对话简述](chat-uuid)
tags:
- tag1
- tag2
affected_files:
- scripts/player/player_controller.gd
- scenes/player/player.tscn
---
<!-- L3 -->
一句话描述改动内容≤80 字,进 headlines
<!-- L2 -->
做了什么 + 为什么3-5 行摘要,进 recent当本条进入最近 20 时被展示)
<!-- L1 -->
- **what**: ...
- **why**: ...
- **decisions**: ...
- **notes**: ...
symptom / root-cause / fix / tests / followups / relation 等按需,正文原样进 full
```
### 上溯原则
- **按需触发**:只有在 L3 信息不足以支撑当前任务判断时才上溯,不预防性地批量读取
- **精准读取**:对 L1 的访问必须通过 Grep 定位行号 + Read 局部读取,禁止全文读取
- **最小化**:一次上溯通常只涉及 1-3 条记录,不批量上溯
字段说明:
- `type``feat` / `fix` / `refactor` / `docs` / `chore` / `infra`(机读,便于统计)
- `merge_source`:本条记录时所在分支(`HEAD` / `master` / 分支名),留空则视图不显示
- `status``active`(默认)/ `reverted` / `superseded`;后两者配 `superseded_by: CL-xxx`
- `affected_files`**必须全路径**(视图 L2 的硬匹配靠它,缩写路径会失效)
- `tags` / `affected_files` 为块列表其余为标量。frontmatter 之外的三段用 `<!-- L1/L2/L3 -->` 分隔
## 操作 A记录写入核心流程
### 触发条件
Agent 完成了一个涉及**代码或配置文件实质性改动**的任务后自动触发。
Agent 完成涉及**代码或配置文件实质性改动**的任务后自动触发。
以下情况**不触发**
- 纯对话讨论、方案设计、问题解答(无文件改动)
- 只读操作(查看文件、搜索代码)
- 只改动了 `.cursor/` 目录下的基础设施文件如画像、延期方案、changelog 自身)
**不触发**:纯对话/方案/答疑(无文件改动)、只读操作、只改 `.cursor/` 下基础设施文件
画像、延期、changelog 自身、hooks、tools 等)。
### 写入流程
```
1. 生成锚点 IDCL-YYYYMMDD-HHMM检查是否与已有 ID 冲突,冲突则追加字母后缀
1. 确定作者 handle见上「作者标识」三步顺序
2. 从刚完成的任务中提取以下信息:
- 做了什么what一句话概括
- 为什么这样做why动机和背景
- 改了哪里where受影响的文件/模块列表
- 关键决策decisions如果有方案选择记录选了什么、放弃了什么
- 注意事项notes后续可能受影响的地方、已知限制等
2. 生成锚点 IDCL-YYYYMMDD-HHMM-<author>
- 检查 entries/ 下是否已存在同 ID含其他作者目录冲突则 HHMM 后加字母后缀
3. 生成三层内容(共享同一个锚点 ID
- L1 完整条目(包含以上全部信息)
- L2 摘要条目what + why + where3-5 行)
- L3 一句话what不超过 80 字)
3. 从刚完成的任务提取what / why / where(affected_files, 全路径) / decisions / notes
并判定 type、merge_source当前分支、tags
4. 写入三个文件(按以下顺序)
a. 读取 changelog-full.md在 "## 记录" 下方追加 L1 条目
b. 读取 changelog-recent.md在顶部插入 L2 条目,如超过 20 条则移除最旧的与下方「L2 窗口」一致)
c. 读取 changelog-headlines.md在顶部插入 L3 条目,如超过 50 条则移除最旧的
4. 用 Write 工具创建单个 fragment 文件
.cursor/changelog/entries/<author>/<id>.md
- 按上面的格式填好 frontmatter + L3/L2/L1 三段
- L3 ≤80 字一句话L2 3-5 行L1 完整正文
5. 在回复末尾附一行提示:"[已记录到开发日志]"
5. 跑生成器重建视图:
python .cursor/changelog/tools/changelog_build.py
Windows 控制台先 set PYTHONIOENCODING=utf-8
6. 在回复末尾附 "[已记录到开发日志]"
```
### 与旧流程的关键区别
- **不再用 StrReplace 往三个文件顶部插入**——只新建一个 fragment 文件,零冲突、零半损坏风险
(旧的 StrReplace 顶部插入曾导致条目半损坏fragment 模型下该风险彻底消失)。
- **不再手动维护滚动窗口**——recent/headlines 的 ~20/~50 截断由生成器自动算。
- **不再手动保持三层同步**——三层都是同一 fragment 的不同密度渲染,天然一致。
### 静默写入原则
- **不需要用户确认**——Agent 自己的改动,对"做了什么"的认知是一手的
- 用户如果觉得记录不准确,可通过操作 D 修改或删除
- 回滚操作也要记录("回退了 XX 改动"),真实反映开发过程
- 不需要用户确认Agent 自己的改动是一手认知)。
- 回滚也要记录(新建一条 `type: chore` / 在被回滚条目里置 `status: reverted` + `superseded_by`)。
## 操作 BL2 自动触发读取
### 触发条件
`changelog-recall.mdc` Rule 调度。当 Agent 开始处理一个新任务时,判断该任务是否
与近期改动相关
### 匹配策略(文件 + 标签双匹配)
```
1. 从当前任务中提取:
- 涉及的文件路径
- 语义关键词(模块名、功能领域等)
2. 读取 changelog-recent.md逐条检查
- 硬匹配:当前任务涉及的文件出现在条目的 affected_files 中
- 软匹配:当前任务的语义关键词与条目的 tags 有交集
3. 任一匹配命中 → 将匹配到的 L2 条目作为上下文纳入考量
4. 在回复中自然融入,不显式提及"根据开发日志"
```
`changelog-recall.mdc` 调度,读取 `changelog-recent.md`
- 硬匹配:当前任务涉及的文件出现在条目 `affected_files`(现为全路径)
- 软匹配:当前任务语义关键词与条目 `tags` 有交集
任一命中即纳入上下文。读取的是**视图**,无需读 fragment
## 操作 CL1 手动检索
### 触发条件
用户要求回顾完整记录时,读取 `changelog-full.md` 并按时间 / 模块 / tags 过滤展示。
用户主动要求回顾完整改动记录时触发。典型话语:
- "回顾一下最近的改动"
- "XX 模块之前改过什么"
- "查看开发日志"
- "changelog"
### 流程
## 逐级上溯机制
L3 信息不足时(语义模糊),按 ID 逐级 Grep
```
1. 读取 changelog-full.md
2. 根据用户需求过滤:
- 按时间范围
- 按模块/文件
- 按 tags
3. 展示匹配的条目摘要表,用户可以进一步查看某条的完整内容
1. 从 L3 提取锚点 ID
2. Grep changelog-recent.mdL2→ 命中读该条
3. 仍不足 → Grep changelog-full.mdL1拿行号Read 该行 ±20 行
(也可直接 Read 对应 fragmententries/*/<id>.md
```
原则:按需触发、精准读取、一次 1-3 条、不全文读 L1。
## 操作 D记录管理
用户可以对已有记录进行管理:
- **修改**:编辑对应 `entries/*/<id>.md` 后跑生成器(不要直接改视图)。
- **删除**:删除对应 fragment 文件后跑生成器。
- **标记取代/回滚**:在 fragment 里置 `status: superseded`/`reverted` + `superseded_by`
| 操作 | 说明 |
## 滚动窗口(自动)
生成器按 ID 时间倒序排序后headlines 取前 50、recent 取前 20、full 全量、by-author 全量。
**无需任何手动修剪**,被移出窗口的条目仍完整保留在 fragment 与 full 视图中。
## git 协作与冲突
- **fragment 几乎不冲突**:不同人写不同文件、不同作者写不同子目录。
- **视图冲突自动吸收**`.gitattributes` 给 4 个视图设 `merge=union`merge 不中断),
pre-commit / post-merge git hook 跑生成器从 fragment **确定性重建**视图,所有人收敛到一致。
- 新克隆需跑一次 `tools/install-git-hooks.ps1`(或 `.sh`)装 hook。
- CI 可加 `changelog_build.py --check` 校验"视图 == 重建(fragment)"防止有人没装 hook 就提交。
- **合并尚未 fragment 化的旧分支**(如重构前的 master`git merge --no-commit --no-ff <分支>`
union 让视图不冲突、`--no-commit` 防止 hook 过早重建),再跑 `tools/merge-import.ps1 [-Ref <分支>]`
(或 `.sh`)把对方旧格式条目收割成 fragment只补缺失 ID、不覆盖已有自动重建并校验无丢失最后 `git add -A; git commit`
- **两条改动撞了同一个 fragment ID**(生成器遇重复 ID 会直接报错):给其中一条改 ID`HHMM` 后加字母,如 `1521``1521a`
文件名与 frontmatter `id` 同步改,再跑生成器。两条都保留、不丢内容。
## 与其他系统协作
| 系统 | 关系 |
|------|------|
| 删除 | 从三层文件中同步移除对应条目 |
| 修改 | 修改某条记录的描述(三层同步更新) |
| 清理 | 手动触发 L1 的归档(如按月分文件,暂不实现,作为演进方向) |
## 条目格式
### L1 完整条目
```markdown
### [CL-20260412-1430] YYYY-MM-DD HH:MM — 一句话标题
- **tags**: tag1, tag2, tag3
- **affected_files**:
- path/to/file1
- path/to/file2
- **what**: 做了什么的简要描述
- **why**: 动机和背景
- **decisions**: 选择了 A 方案(放弃了 B 因为 xxx
- **notes**: 后续注意事项
- **source_chat**: [对话简述](chat-uuid)
```
### L2 摘要条目
```markdown
### [CL-20260412-1430] YYYY-MM-DD — 一句话标题
- **tags**: tag1, tag2, tag3
- **affected_files**: file1, file2
- **summary**: 做了什么 + 为什么3-5 行)
```
### L3 一句话条目
```markdown
- [CL-20260412-1430] 一句话描述改动内容(不超过 80 字)
```
## 滚动窗口维护
### L2 窗口(~20 条)
```
写入新条目后,检查总条目数:
- <= 20 条:不做处理
- > 20 条:移除文件底部(最旧的)条目,直到恰好 20 条
```
### L3 窗口(~50 条)
```
写入新条目后,检查总行数(排除文件头部的标题和说明):
- <= 50 条:不做处理
- > 50 条:移除文件底部(最旧的)条目,直到恰好 50 条
```
被移除的条目不需要额外归档——L1 完整版保留了所有历史。
## 数据文件模板
首次写入时,如对应文件不存在,按以下模板创建。
### changelog-full.md
```markdown
# Dev Changelog — Full
完整的开发改动记录,按时间倒序排列。作为主动 RAG 的数据源,用户手动唤醒时读取。
## 记录
```
### changelog-recent.md
```markdown
# Dev Changelog — Recent
最近 ~10 次改动的摘要记录,按时间倒序排列。
当 Agent 检测到当前任务与近期改动相关时自动读取。
```
### changelog-headlines.md
```markdown
# Dev Changelog — Headlines
最近 ~50 次改动的一句话概要,按时间倒序排列。每次会话自动注入上下文。
```
## 与其他系统的协作
| 系统 | 关系 | 说明 |
|------|------|------|
| `changelog-recall` Rule | 下游消费者 | 每次会话注入 L3自动触发 L2 读取 |
| `deferred-decisions` Skill | 互补 | deferred 记"没做什么"changelog 记"做了什么" |
| `profile-memory` Skill | 结构对称 | profile 是"是什么"changelog 是"做了什么" |
| `epee-orchestrator` | 注册 | 在 registry.md 中注册本 Skill |
| `changelog-recall` Rule | 下游消费者:注入 L3、按需读 L2、上溯 L1 |
| `deferred-decisions` | 互补:记"没做什么" |
| `profile-memory` | 结构对称:记"是什么" |
| `epee-orchestrator` | 在 registry.md 注册本 Skill |
## 自迭代日志
本节记录使用本 Skill 过程中发现的必要检查项。
### 已知必要检查
1. **大任务收尾遗漏风险**单次任务涉及 5+ 文件改动时,Agent 容易在输出总结回复后遗漏被动写入流程。应在生成最终回复前,先执行 `changelog-recall.mdc` 中的"任务完成 Checklist",确认三层日志已写入后再输出回复。绝不能"先回复再补写"。
1. **大任务收尾不要遗漏写入** — 单次 5+ 文件改动时,先按 `changelog-recall.mdc`
"任务完成 Checklist" 写完 fragment + 跑生成器,再输出回复;绝不"先回复再补写"。
2. **L3/L2 滚动窗口必须落地** — 操作 A 第 4 步不是「插入即结束」写入后必须数清条目L3 为以 `- [` 开头的列表行L2 为 `### [CL-` 标题行。L3 超过 50 条、L2 超过 20 条时,必须从**文件底部(最旧)**整段删除条目直至恰好满足上限;禁止长期只追加不修剪。若发现 `changelog-headlines.md` 列表行多于 50说明上次写入未执行本项本次补修剪并自检。
2. **affected_files 必须全路径** — 视图 L2 的硬匹配依赖全路径;缩写路径(如只写文件名)
会让 recall 的硬匹配失效。迁移前的旧 L2 缩写路径问题已由本次重构(从 frontmatter 全路径渲染)解决。
3. **顶部插入 StrReplace 范式L1/L2** — L1 和 L2 的每个条目都是多行结构(标题 + 多条 `- **字段**:`)。使用 StrReplace 在顶部插入新条目时anchor`old_string`)有且仅有两种合法选择:
(a) 只包含新条目之前的"稳定前缀"(如 L1 的 `## 记录\n\n` 或 L2 的整段文件头说明),**不触及任何已有条目的任何一行**`new_string` = 稳定前缀 + 新条目完整内容 + 空行
(b) 把旧首条目的**完整多行内容**(标题行 + 全部字段行)都纳入 `old_string``new_string` = 新条目完整内容 + 空行 + 旧首条目完整内容。
3. **写完务必跑生成器** — 只建 fragment 不跑 `changelog_build.py`视图就不会更新recall 读的是视图)。
pre-commit hook 会兜底重建,但当前会话内的 recall 注入读的是已落盘视图,故应即时生成
**绝对禁止**:只把旧首条目的标题行(或前 1-2 字段)当 anchor 而不带上剩余字段行——StrReplace 会吞掉这些 anchor 行但不动剩余字段,造成"旧标题丢失、字段游离"的半损坏状态2026-04-20 在 `pitfall-journal` 的写入中已实际踩过此坑)。
4. **作者 handle 要稳定** — 优先用 `.cursor/local-env.json``changelog-author`,避免每次从
`git config user.name` 推导导致 handle 漂移(同一人出现多个目录)。
**写入后自检**Read 文件头部 30 行,确认看到新条目完整 + 旧首条目标题行仍存在且字段完整跟随。L3 的一行条目不受本条约束(它就是单行,天然无此风险)。
5. **Windows 控制台编码** — 跑脚本前 `set PYTHONIOENCODING=utf-8`(或 PowerShell `$env:PYTHONIOENCODING="utf-8"`
否则中文输出在 GBK 控制台显示乱码(不影响 UTF-8 文件写入,仅影响 stdout 观感)。

View File

@@ -93,7 +93,8 @@ description: >-
### 条目格式
参见 [registry.md](registry.md) 中的条目结构。
参见 [registry.md](registry.md) 中的条目结构。Registry 条目必须对应磁盘上真实存在的
Skill删除 Skill 时同步移除条目。
## Skill 创建价值判断标准
@@ -114,9 +115,8 @@ description: >-
|------|------|------|
| `create-skill` Skill | 下游委托 | 操作 B 创建流程的执行者 |
| `deferred-decisions` Skill | 下游工具 | 操作 B 延期记录的执行者 |
| `skill-self-iteration` Rule | 并行 | 它负责单个 Skill 的经验积累Orchestrator 负责 Skill 间的调度 |
| `deferred-recall` Rule | 协同 | Orchestrator 延期的建议通过 deferred-recall 在未来自动提醒 |
| `forskill-convention` Rule | 兼容 | 新 Skill 如需 Editor 脚本,仍遵循 ForSkill 约定 |
| `deferred-recall` Rule | 协同 | 延期建议通过 deferred-recall 在未来自动提醒 |
| `cursor-init` Skill | 校准 | 初始化时把 Registry 校准为实际 8 个 Skill |
## 自迭代日志
@@ -124,4 +124,6 @@ description: >-
### 已知必要检查
1. **收尾动作级联遗漏** — 当 Agent 遗漏了一个收尾动作(如 changelog 写入)后,后续的收尾动作自迭代、Registry 同步)也会被一并遗漏,因为它们都在同一个"收尾阶段"。`changelog-recall.mdc` 中的 Checklist 化设计可以打断这种级联——每项独立检查,不依赖前一项的执行记忆。
1. **收尾动作级联遗漏** — 当 Agent 遗漏了一个收尾动作(如 changelog 写入)后,
后续的 Registry 同步也容易遗漏。每次创建、删除或实质修改 Skill 后都应把
Registry 同步作为独立检查项。

View File

@@ -1,17 +1,15 @@
# EPEE Skill Registry
> 本文件由 EPEE Skill Orchestrator 维护,记录所有已实现 Skill 的摘要信息
> 每次创建/修改 Skill 后必须同步更新(参见 `epee-orchestrator.mdc` Rule
> 本文件由 EPEE Skill Orchestrator 维护,只登记磁盘上真实存在的 Skill
> 创建、删除或实质修改 Skill 后必须同步更新。
## 条目格式说明
## 条目格式
每个条目包含以下字段:
- **类型**项目级 / 个人级 / 基础设施
- **能力**1 句话核心能力描述
- **触发场景**:逗号分隔的关键词/短语,用于与任务特征匹配
- **输出**该 Skill 的产出物
- **路径**SKILL.md 的相对路径
- **备注**(可选):特殊说明
- **类型**:基础设施 / 个人级
- **能力**该 Skill 做什么
- **触发场景**:何时使用
- **输出**:实际产出
- **路径**存在的 `SKILL.md` 相对路径
---
@@ -19,52 +17,56 @@
### cursor-init
- **类型**: 基础设施
- **能力**: 把从模板 clone 或复制过来的 .cursor/ 重置为新项目的干净起点——按 baserule 归位原有 rules/skills清理源项目的项目专用数据重建 registry交互式生成 .gitignore最后写入 `.cursor/.init-done` sentinel
- **触发场景**: 首次会话自动触发(.cursor/.init-done 缺失时由 cursor-init-recall 规则引导);关键词:"初始化cursor"、"cursor init"、"重置 cursor"、"reset cursor"、"按 baserule 归位"、"补 gitignore"、"生成 gitignore"、"gitignore 模板"
- **输出**: 清理后`.cursor/` 结构规范清单完整、项目级内容已清、数据文件为模板、registry 仅含通用条目)、项目根 `.gitignore`(可选)`.cursor/.init-done` sentinel
- **能力**: 以精确 canonical 白名单初始化或重置 `.cursor/`,保留 Godot baseline清理历史运行数据生成本机配置与 `.gitignore`,并在验证成功后写 sentinel
- **触发场景**: 首次缺少 `.cursor/.init-done`"初始化 cursor"、"cursor init"、"重置 cursor"、"补 gitignore"
- **输出**: 干净且通过验证`.cursor/` 基线、gitignored `local-env.json`、可选 `.gitignore``.cursor/.init-done`
- **路径**: .cursor/skills/cursor-init/SKILL.md
- **备注**: 首次通过 sentinel 自动触发一次,之后仅关键词手动触发;破坏性操作必须 dry-run设计为幂等重复运行无害阶段 7 失败时不写 sentinel 保证可恢复
### deferred-decisions
- **类型**: 基础设施
- **能力**: 记录和追踪延期的技术方案/决策,在关联任务出现时主动提醒
- **能力**: 记录和追踪延期的技术方案决策,在关联任务出现时提醒
- **触发场景**: "以后再做"、"先不做"、"defer"、延期方案管理、延期回顾
- **输出**: .cursor/deferred/registry.md 条目
- **输出**: `.cursor/deferred/registry.md` 条目及状态更新
- **路径**: .cursor/skills/deferred-decisions/SKILL.md
- **备注**: 不参与任务分流匹配,仅作为 Orchestrator 的下游工具
### dev-changelog
- **类型**: 基础设施
- **能力**: 三层开发进程记录系统Agent 完成代码改动后自动记录,提供一句话概要到完整日志的多级上下文
- **触发场景**: "开发日志"、"changelog"、"最近改了什么"、"回顾改动"、"查看开发记录"、代码改动后自动写入
- **输出**: .cursor/changelog/ 下的 changelog-full.md、changelog-recent.md、changelog-headlines.md
- **能力**: 以一条一文件的 fragment 记录开发改动,并确定性重建完整、近期、概要和作者四个视图
- **触发场景**: 实质代码或配置改动完成后、"开发日志"、"changelog"、"最近改了什么"、"回顾改动"
- **输出**: `.cursor/changelog/entries/<author>/<id>.md` 与四个生成视图
- **路径**: .cursor/skills/dev-changelog/SKILL.md
### epee-orchestrator
- **类型**: 基础设施
- **能力**: 匹配已有 Skill、发现可复用工作流缺口并维护与磁盘一致的 Skill Registry
- **触发场景**: 手动配置密集、重复模式明确、步骤繁多、Skill 缺口发现、Registry 同步
- **输出**: Skill 分流或创建建议,以及 `.cursor/skills/epee-orchestrator/registry.md` 更新
- **路径**: .cursor/skills/epee-orchestrator/SKILL.md
### pitfall-journal
- **类型**: 基础设施
- **能力**: 记录开发中踩过的坑(非显而易见问题),在遇到同类问题时自动检索匹配已有经验
- **触发场景**: debug 完成、"踩坑"、"之前遇到过"、"坑"、进入 Debug mode、同类错误反复出现
- **输出**: .cursor/pitfalls/pitfalls.md 条目
- **能力**: 记录非显而易见问题的症状、根因、解法和防御措施,并在同类问题出现时检索
- **触发场景**: debug 完成、问题反复出现、"踩坑"、"之前遇到过"、进入 Debug mode
- **输出**: `.cursor/pitfalls/pitfalls.md` 条目或匹配到的既有经验
- **路径**: .cursor/skills/pitfall-journal/SKILL.md
### problem-distillery
- **类型**: 基础设施
- **能力**: 追踪反复出现的顽固问题,记录解决过程和弯路,定期蒸馏精炼方法论,经实践验证后自动升级为新会话常规注入
- **触发场景**: "还有问题"、"没解决"、"还是一样"、"又出现了"、"试了好多次"、"反复出现"、Agent 意识到同一问题尝试 2 次以上未解决、"蒸馏"、"总结经验"
- **输出**: .cursor/distillery/ 下的 problems.mdinsights.mdgolden-rules.md
- **能力**: 追踪反复未解决的问题,蒸馏方法论,并把经验证的 insight 升级为 Golden Rule
- **触发场景**: "还有问题"、"没解决"、"还是一样"、同一问题尝试两次以上、"蒸馏"、"总结经验"
- **输出**: `.cursor/distillery/problems.md``insights.md``golden-rules.md`
- **路径**: .cursor/skills/problem-distillery/SKILL.md
### profile-memory
- **类型**: 个人级
- **能力**: 在对话中被动检测用户个人特质和项目信息,经确认持久化为精简画像和详细日志
- **触发场景**: "画像"、"profile"、"我的偏好"、"查看画像"、"项目信息"、"查看项目画像"、用户主动管理画像
- **输出**: ~/.cursor/profile/user-profile.md、.cursor/profile/project-profile.md 及对应 log 文件
- **能力**: 检测并经确认持久化用户偏好与项目画像,支持查看、修改、删除和溯源
- **触发场景**: 持久偏好或项目决策出现、"画像"、"profile"、"我的偏好"、"查看项目画像"
- **输出**: 用户与项目的精简 Profile 及详细 Log
- **路径**: .cursor/skills/profile-memory/SKILL.md
### epee-orchestrator
- **类型**: 基础设施
- **能力**: 元层调度系统,检测任务是否可由已有 Skill 处理并分流,或发现 Skill 缺口并引导创建新 Skill同时负责 Skill Registry 维护
- **触发场景**: 手动配置密集、重复模式明确、手工指引过长的任务、Skill 缺口发现、Registry 同步维护
- **输出**: 分流决策(调用已有 Skill / 创建新 Skill、.cursor/skills/epee-orchestrator/registry.md 维护
- **路径**: .cursor/skills/epee-orchestrator/SKILL.md
- **备注**: 元层 Skill自身注册以便其他 Agent 了解调度能力的可用性
### html-doc
- **类型**: 个人级
- **能力**: 生成统一深色主题的单文件 HTML 文档,并可选提供本地暂存和回传批注功能
- **触发场景**: HTML 参考手册、说明页、报告、清单、统一 HTML 样式、可批注文档
- **输出**: 内联 CSS/JS 的自包含 `.html` 文档
- **路径**: .cursor/skills/html-doc/SKILL.md

View File

@@ -0,0 +1,88 @@
---
name: html-doc
description: >-
生成人类可读的单文件 HTML 文档(参考手册、报告、字段说明、清单等),统一深色主题
与版式(侧边目录 + 表格 + callout + 徽标 + 代码块),并可选挂载标准化的「批注模块」——
让读者逐字段/逐区块填备注、自动暂存、一键导出带备注的 HTML 副本回传,作者打开即见全部批注。
当用户要「做一份给人看的 HTML 文档/参考手册/说明页/报告」,或提到「统一 HTML 样式」、
「让别人写批注/注释」、「可批注文档」时使用。
---
# html-doc
把「给人阅读的 HTML 文档」标准化:统一主题 + 统一版式 + 可选批注模块。**产出单文件自包含 HTML**CSS/JS 全内联,一个 `.html` 直接发人)。
## 资产
| 文件 | 作用 | 生成时怎么用 |
|------|------|------|
| `assets/theme.css` | 通用深色主题tokens + 版式 + 表格/callout/徽标/目录/代码块) | 内联进 `<head><style>` |
| `assets/annotate.js` | 标准化批注模块(表格列 / 区块框、localStorage 暂存、导出嵌 JSON、复制 Markdown | 需批注时内联进末尾 `<script>` |
| `templates/skeleton.html` | 文档骨架(侧栏目录 + 正文 + 占位符) | 复制后填内容 |
> 三者是唯一真相源;改主题/批注逻辑只改 assets再重新生成文档。
## 生成工作流
```
1. 明确文档类型与章节结构(参考手册 / 报告 / 清单…)
2. 复制 templates/skeleton.html替换占位符
{{TITLE}} {{SUBTITLE}} {{DOC_ID}}(批注命名空间,用短横线英文,如 csv-data-reference
{{TOC_LINKS}} {{CONTENT}} {{FOOTER}}
3. 把 assets/theme.css 全部内容内联到 <head> 的 <style> 里(单文件自包含)
4. 按「组件速查」填正文;每章一个 <section id="...">,与目录 <a href="#..."> 对应
5. 若需批注 → 见「批注模块」:给目标加 data 属性 + 内联 annotate.js
6. 用 Read/ReadLints 自查;中文内容务必用文件工具写(勿用 PowerShell 重定向,见 baserule
```
## 组件速查
章节标题:`<h2 class="section">`(自带顶部分隔线);子标题 `<h3>`
目录项:`<a href="#id">主名<span class="cn">副名</span></a>`;分隔 `<div class="sep"></div>`
表格:标准 `<table><thead><tr><th>…</thead><tbody>…`。类型列用 `<td class="type">`
徽标(语义色,放 `<span class="badge X">``accent` / `ok` / `warn` / `danger` / `muted`
提示框:
```html
<div class="callout warn"><div class="t">标题</div><p>正文</p></div>
```
变体:默认(蓝)/ `warn` / `danger` / `ok`
其他:`.tag`(等宽 pill`.legend`(图例行)、`ul.tight`(紧凑列表)、`.muted`(弱化文字)、`<footer>`
配色改动只动 `theme.css``:root` design tokens。文档专属徽标追加在内联 theme 之后。
## 批注模块(可选)
目的:读者填备注 → 导出带备注的 HTML 发回 → 作者打开即见。纯前端、零依赖、幂等。
**接入三步**
1. `<body data-note-ns="文档唯一标识">`(骨架已留 `{{DOC_ID}}`;隔离不同文档的本地暂存)
2. 标记批注目标(二选一或混用):
- 表格模式:`<table data-annotate="唯一名">` → 每行自动追加一列「备注」
- 区块模式:任意元素加 `data-note-id="唯一名"` → 其后插入一个备注框
- 防行序漂移:可给某 `<tr>``data-note-id="稳定key"` 覆盖自动 key
3.`assets/annotate.js` 全部内容内联进末尾 `<script>`
**运行行为**:页头下方出现工具栏(导出带备注副本 / 复制 Markdown / 清空 / 已填计数);输入自动存 localStorage导出时克隆文档、剔除注入 UI、仅把备注以 `<script id="embedded-notes" type="application/json">` 嵌入 → 对方打开由脚本幂等重建并回填。
**恢复优先级**:嵌入 JSON > localStorage保证收到的回传文件一定显示对方批注
## 反模式
- 别用外链 CSS/JS破坏「一个文件发人」——一律内联。
- 别硬编码猜测批注目标(如靠表头文字)——用 `data-annotate` / `data-note-id` 显式 opt-in。
- 别手改 `embedded-notes` JSON —— 由模块导出维护。
- 中文文本别用 PowerShell `Set-Content`/`echo >` 落盘GBK 会损坏),用文件工具写。
## 自迭代日志
本节记录使用本 Skill 过程中发现的必要检查项。
### 已知必要检查
1. **单文件自包含** — 生成后确认 `theme.css`(及批注时的 `annotate.js`)已真正内联,页面无 `<link>`/外链 `<script src>`,否则发给别人会样式/功能丢失。
2. **批注命名空间唯一**`data-note-ns` 必须每份文档不同,否则不同文档的 localStorage 暂存互相污染。

View File

@@ -0,0 +1,232 @@
/* ============================================================
html-doc · 批注模块annotate.js
让读者在文档里逐字段/逐区块填备注,自动暂存到 localStorage
一键导出「把备注嵌进 JSON 的 HTML 副本」发回,对方打开即可看到全部批注。
用法(生成单文件文档时,把本文件全部内联进 <script>
1) 表格模式:给 <table> 加 data-annotate="唯一名",会为每行追加一列「备注」。
- 可选:给某个 <tr> 加 data-note-id="稳定key" 覆盖自动 key防行序漂移
2) 区块模式:给任意元素加 data-note-id="稳定key",其后会插入一个备注框。
3) 命名空间:给 <body> 加 data-note-ns="文档唯一标识",隔离不同文档的暂存。
纯前端、零依赖、幂等(重复打开导出的副本不会重复注入)。
============================================================ */
(function () {
"use strict";
var NS = (document.body && document.body.dataset.noteNs) || document.title || "html-doc";
var STORAGE_PREFIX = "htmldoc-note::" + NS + "::";
var NOTE_COL_LABEL = "备注 / 批注";
// 1. 读取上一份文件嵌入的备注(对方发回来的文件里带着)
var embedded = {};
var embEl = document.getElementById("embedded-notes");
if (embEl) {
try { embedded = JSON.parse(embEl.textContent || "{}"); } catch (e) { embedded = {}; }
}
// 2. 注入自带样式
var style = document.createElement("style");
style.id = "hd-note-style";
style.textContent = [
".hd-note-toolbar{position:sticky;top:0;z-index:30;display:flex;align-items:center;gap:10px;flex-wrap:wrap;",
"background:var(--panel,#171a21);border:1px solid var(--border,#2a2f3a);border-radius:10px;padding:12px 16px;",
"margin:0 0 24px;box-shadow:0 6px 18px rgba(0,0,0,.35)}",
".hd-note-toolbar .nt-title{font-weight:600;font-size:14px}",
".hd-note-toolbar .nt-count{color:var(--accent-2,#7ee0a2);font-size:12.5px}",
".hd-note-toolbar .nt-hint{color:var(--text-dim,#9aa1ad);font-size:12.5px;flex-basis:100%;margin-top:2px}",
".hd-note-btn{background:var(--panel-2,#1e222b);color:var(--text,#d7dbe2);border:1px solid var(--border,#2a2f3a);",
"border-radius:7px;padding:6px 12px;font-size:13px;cursor:pointer;font-family:inherit}",
".hd-note-btn:hover{border-color:var(--accent,#5aa9ff);color:var(--accent,#5aa9ff)}",
".hd-note-btn.primary{background:rgba(90,169,255,.14);border-color:rgba(90,169,255,.5);color:var(--accent,#5aa9ff)}",
".hd-note-btn.danger:hover{border-color:var(--danger,#ff6b6b);color:var(--danger,#ff6b6b)}",
"th.hd-note-col,td.hd-note-cell{min-width:200px}",
"textarea.hd-note{width:100%;background:var(--code-bg,#11141a);color:var(--text,#d7dbe2);",
"border:1px solid var(--border,#2a2f3a);border-radius:6px;padding:6px 8px;font-family:inherit;font-size:13px;",
"line-height:1.5;resize:vertical;min-height:38px}",
"textarea.hd-note:focus{outline:none;border-color:var(--accent,#5aa9ff)}",
"textarea.hd-note.filled{border-color:rgba(126,224,162,.5);background:rgba(126,224,162,.06)}",
".hd-note-block{margin:8px 0 18px}",
".hd-note-block .lbl{font-size:12px;color:var(--text-dim,#9aa1ad);margin-bottom:4px}"
].join("");
document.head.appendChild(style);
var entries = []; // {key, label, group, ta}
function restore(key) {
if (Object.prototype.hasOwnProperty.call(embedded, key)) return embedded[key]; // 收到的文件优先显示对方批注
var ls = localStorage.getItem(STORAGE_PREFIX + key);
return ls !== null ? ls : "";
}
function makeTextarea(key, label, group) {
var ta = document.createElement("textarea");
ta.className = "hd-note";
ta.rows = 2;
ta.placeholder = "在此填写备注…";
ta.dataset.key = key;
var val = restore(key);
ta.value = val;
if (val.trim()) ta.classList.add("filled");
ta.addEventListener("input", function () {
localStorage.setItem(STORAGE_PREFIX + key, ta.value);
ta.classList.toggle("filled", ta.value.trim() !== "");
updateCount();
});
entries.push({ key: key, label: label, group: group, ta: ta });
return ta;
}
// 3a. 表格模式table[data-annotate]
var annTables = document.querySelectorAll("table[data-annotate]");
Array.prototype.forEach.call(annTables, function (table, ti) {
var tableKey = table.getAttribute("data-annotate") || ("table" + ti);
var section = table.closest("section");
var h2 = section ? section.querySelector("h2.section, h2, h3") : null;
var group = h2 ? h2.textContent.trim() : tableKey;
var headRow = table.querySelector("thead tr");
if (headRow) {
var th = document.createElement("th");
th.className = "hd-note-col";
th.textContent = NOTE_COL_LABEL;
headRow.appendChild(th);
}
var rows = table.querySelectorAll("tbody tr");
Array.prototype.forEach.call(rows, function (tr, idx) {
var firstCell = tr.querySelector("td");
var label = firstCell ? firstCell.textContent.trim() : ("行" + idx);
var key = tr.getAttribute("data-note-id") || (tableKey + "::" + idx + "::" + label);
var td = document.createElement("td");
td.className = "hd-note-cell";
td.appendChild(makeTextarea(key, label, group));
tr.appendChild(td);
});
});
// 3b. 区块模式:任意 [data-note-id](表格行已在上面处理,这里排除)
var blocks = document.querySelectorAll("[data-note-id]");
Array.prototype.forEach.call(blocks, function (el) {
if (el.tagName === "TR" || el.closest("table[data-annotate]")) return;
var key = el.getAttribute("data-note-id");
var section = el.closest("section");
var h2 = section ? section.querySelector("h2.section, h2, h3") : null;
var group = h2 ? h2.textContent.trim() : "区块批注";
var label = (el.textContent || "").trim().slice(0, 40) || key;
var wrap = document.createElement("div");
wrap.className = "hd-note-block";
var lbl = document.createElement("div");
lbl.className = "lbl";
lbl.textContent = NOTE_COL_LABEL;
wrap.appendChild(lbl);
wrap.appendChild(makeTextarea(key, label, group));
if (el.nextSibling) el.parentNode.insertBefore(wrap, el.nextSibling);
else el.parentNode.appendChild(wrap);
});
if (!entries.length) return; // 没有任何批注目标就不加工具栏
// 4. 工具栏
var bar = document.createElement("div");
bar.className = "hd-note-toolbar";
bar.innerHTML =
'<span class="nt-title">批注模式</span>' +
'<span class="nt-count" id="hdNoteCount">已填 0 条</span>' +
'<button class="hd-note-btn primary" id="hdBtnExport">导出带备注的副本 (.html)</button>' +
'<button class="hd-note-btn" id="hdBtnMd">复制备注为 Markdown</button>' +
'<button class="hd-note-btn danger" id="hdBtnClear">清空本地备注</button>' +
'<span class="nt-hint">填写会自动暂存到本机浏览器;填完点「导出带备注的副本」下载一份 HTML 发回即可,对方打开就能看到所有批注。</span>';
var main = document.querySelector("main") || document.body;
var head = main.querySelector("header.page-head");
if (head && head.nextSibling) main.insertBefore(bar, head.nextSibling);
else main.insertBefore(bar, main.firstChild);
function collect() {
var out = {};
entries.forEach(function (e) { if (e.ta.value.trim() !== "") out[e.key] = e.ta.value; });
return out;
}
function updateCount() {
var n = entries.filter(function (e) { return e.ta.value.trim() !== ""; }).length;
var el = document.getElementById("hdNoteCount");
if (el) el.textContent = "已填 " + n + " 条";
}
updateCount();
function pad(n) { return (n < 10 ? "0" : "") + n; }
function stamp() {
var d = new Date();
return "" + d.getFullYear() + pad(d.getMonth() + 1) + pad(d.getDate()) + "-" + pad(d.getHours()) + pad(d.getMinutes());
}
function flash(btn, msg) {
if (!btn) return;
var old = btn.textContent;
btn.textContent = msg;
setTimeout(function () { btn.textContent = old; }, 1500);
}
// 5. 导出:克隆文档 → 剔除注入的 UI → 仅嵌入备注 JSON再次打开由本脚本幂等重建
function exportHtml() {
var data = collect();
var clone = document.documentElement.cloneNode(true);
clone.querySelectorAll(".hd-note-col,.hd-note-cell,.hd-note-block,.hd-note-toolbar,#hd-note-style,#embedded-notes")
.forEach(function (el) { el.parentNode.removeChild(el); });
var s = document.createElement("script");
s.type = "application/json";
s.id = "embedded-notes";
s.textContent = JSON.stringify(data, null, 2);
clone.querySelector("body").appendChild(s);
var html = "<!DOCTYPE html>\n" + clone.outerHTML;
var blob = new Blob([html], { type: "text/html;charset=utf-8" });
var a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = "notes-" + NS.replace(/[^a-zA-Z0-9_-]+/g, "_") + "-" + stamp() + ".html";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(a.href);
}
// 6. 复制为 Markdown按 group 分组)
function toMarkdown() {
var byGroup = {}, order = [];
entries.forEach(function (e) {
if (e.ta.value.trim() === "") return;
if (!byGroup[e.group]) { byGroup[e.group] = []; order.push(e.group); }
byGroup[e.group].push("- **" + e.label + "**: " + e.ta.value.replace(/\n+/g, " "));
});
if (!order.length) return "(暂无备注)";
var lines = ["# " + NS + " · 批注", ""];
order.forEach(function (g) { lines.push("## " + g); lines.push.apply(lines, byGroup[g]); lines.push(""); });
return lines.join("\n");
}
function copyMd() {
var md = toMarkdown();
var done = function () { flash(document.getElementById("hdBtnMd"), "已复制 Markdown"); };
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(md).then(done, function () { fallbackCopy(md, done); });
} else { fallbackCopy(md, done); }
}
function fallbackCopy(text, cb) {
var t = document.createElement("textarea");
t.value = text; t.style.position = "fixed"; t.style.opacity = "0";
document.body.appendChild(t); t.select();
try { document.execCommand("copy"); } catch (e) {}
document.body.removeChild(t); if (cb) cb();
}
function clearAll() {
if (!window.confirm("确定清空本机暂存的所有备注?(不影响已导出的文件)")) return;
entries.forEach(function (e) {
localStorage.removeItem(STORAGE_PREFIX + e.key);
e.ta.value = ""; e.ta.classList.remove("filled");
});
updateCount();
}
var be = document.getElementById("hdBtnExport");
var bm = document.getElementById("hdBtnMd");
var bc = document.getElementById("hdBtnClear");
if (be) be.addEventListener("click", exportHtml);
if (bm) bm.addEventListener("click", copyMd);
if (bc) bc.addEventListener("click", clearAll);
})();

View File

@@ -0,0 +1,195 @@
/* ============================================================
html-doc · 通用深色主题
人类可读 HTML 文档的统一表现层。生成单文件文档时,把本文件
全部内容内联进 <style>。design tokens 集中在 :root改配色只动这里。
============================================================ */
:root {
--bg: #0f1115;
--panel: #171a21;
--panel-2: #1e222b;
--border: #2a2f3a;
--text: #d7dbe2;
--text-dim: #9aa1ad;
--accent: #5aa9ff;
--accent-2: #7ee0a2;
--warn: #ffb454;
--danger: #ff6b6b;
--code-bg: #11141a;
--mono: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
--sans: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
background: var(--bg);
color: var(--text);
font-family: var(--sans);
line-height: 1.6;
font-size: 15px;
}
/* ---------- 布局:侧边目录 + 正文 ---------- */
.layout { display: flex; align-items: flex-start; }
nav.toc {
position: sticky;
top: 0;
align-self: flex-start;
width: 260px;
height: 100vh;
overflow-y: auto;
background: var(--panel);
border-right: 1px solid var(--border);
padding: 20px 16px;
flex-shrink: 0;
}
nav.toc h2 {
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-dim);
margin: 0 0 12px;
}
nav.toc a {
display: block;
color: var(--text);
text-decoration: none;
padding: 6px 10px;
border-radius: 6px;
font-size: 14px;
margin-bottom: 2px;
}
nav.toc a:hover { background: var(--panel-2); color: var(--accent); }
nav.toc a .cn { color: var(--text-dim); font-size: 12px; margin-left: 6px; }
nav.toc .sep { height: 1px; background: var(--border); margin: 12px 4px; }
main {
flex: 1;
max-width: 1080px;
margin: 0 auto;
padding: 40px 48px 120px;
min-width: 0;
}
/* ---------- 标题与正文 ---------- */
header.page-head { margin-bottom: 32px; }
header.page-head h1 { font-size: 30px; margin: 0 0 8px; }
header.page-head p.sub { color: var(--text-dim); margin: 0; }
h2.section {
font-size: 24px;
margin: 48px 0 6px;
padding-top: 12px;
border-top: 1px solid var(--border);
}
h2.section:first-of-type { border-top: none; }
h3 { font-size: 19px; margin: 32px 0 10px; }
.file-title { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; }
.file-title code { font-size: 18px; }
.cnname { color: var(--text-dim); font-size: 15px; }
p { margin: 10px 0; }
a.link { color: var(--accent); }
.muted { color: var(--text-dim); }
/* ---------- 代码 ---------- */
code {
font-family: var(--mono);
background: var(--code-bg);
border: 1px solid var(--border);
border-radius: 4px;
padding: 1px 5px;
font-size: 13px;
color: #e6c07b;
}
pre {
background: var(--code-bg);
border: 1px solid var(--border);
border-radius: 8px;
padding: 14px 16px;
overflow-x: auto;
font-family: var(--mono);
font-size: 13px;
line-height: 1.55;
}
pre code { background: none; border: none; padding: 0; color: var(--text); }
/* ---------- 表格 ---------- */
table {
width: 100%;
border-collapse: collapse;
margin: 14px 0 6px;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 8px;
overflow: hidden;
font-size: 14px;
}
thead th {
text-align: left;
background: var(--panel-2);
color: var(--text-dim);
font-weight: 600;
padding: 10px 12px;
border-bottom: 1px solid var(--border);
white-space: nowrap;
}
tbody td { padding: 9px 12px; border-bottom: 1px solid var(--border); vertical-align: top; }
tbody tr:last-child td { border-bottom: none; }
tbody tr:hover { background: var(--panel-2); }
td.type { color: var(--warn); font-family: var(--mono); font-size: 12.5px; white-space: nowrap; }
/* ---------- 徽标(语义色)---------- */
.badge {
display: inline-block;
font-size: 11px;
padding: 1px 7px;
border-radius: 999px;
border: 1px solid var(--border);
white-space: nowrap;
line-height: 1.5;
}
.badge.accent { background: rgba(90,169,255,0.14); color: var(--accent); border-color: rgba(90,169,255,0.4); }
.badge.ok { background: rgba(126,224,162,0.12); color: var(--accent-2); border-color: rgba(126,224,162,0.35); }
.badge.warn { background: rgba(255,180,84,0.10); color: var(--warn); border-color: rgba(255,180,84,0.3); }
.badge.danger { background: rgba(255,107,107,0.12); color: var(--danger); border-color: rgba(255,107,107,0.4); }
.badge.muted { background: rgba(154,161,173,0.10); color: var(--text-dim); }
/* ---------- 提示框 ---------- */
.callout {
border-left: 3px solid var(--accent);
background: var(--panel);
border-radius: 0 8px 8px 0;
padding: 12px 16px;
margin: 16px 0;
}
.callout.warn { border-left-color: var(--warn); }
.callout.danger { border-left-color: var(--danger); }
.callout.ok { border-left-color: var(--accent-2); }
.callout .t { font-weight: 600; margin-bottom: 4px; }
/* ---------- 通用 pill / 标签 ---------- */
.tag {
display: inline-block;
background: var(--panel-2);
border: 1px solid var(--border);
border-radius: 6px;
padding: 3px 9px;
font-family: var(--mono);
font-size: 12.5px;
color: var(--accent);
margin: 4px 6px 4px 0;
}
/* ---------- 图例 / 列表 ---------- */
.legend { display: flex; gap: 14px; flex-wrap: wrap; margin: 10px 0 0; font-size: 13px; color: var(--text-dim); }
.legend span { display: inline-flex; align-items: center; gap: 6px; }
ul.tight { margin: 8px 0; padding-left: 22px; }
ul.tight li { margin: 3px 0; }
footer { margin-top: 60px; padding-top: 20px; border-top: 1px solid var(--border); color: var(--text-dim); font-size: 13px; }
@media (max-width: 860px) {
nav.toc { display: none; }
main { padding: 24px 18px 80px; }
}

View File

@@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{TITLE}}</title>
<style>
/* [html-doc] 生成时:把 assets/theme.css 的全部内容内联到这里(保证单文件自包含)。
如需文档专属徽标/样式,追加在 theme.css 内容之后即可。 */
</style>
</head>
<body data-note-ns="{{DOC_ID}}">
<div class="layout">
<nav class="toc">
<h2>目录</h2>
<!-- {{TOC_LINKS}} 例:<a href="#sec1">章节一<span class="cn">副标题</span></a>
分隔线用 <div class="sep"></div> -->
</nav>
<main>
<header class="page-head">
<h1>{{TITLE}}</h1>
<p class="sub">{{SUBTITLE}}</p>
</header>
<!-- {{CONTENT}}
每个章节: <section id="sec1"><h2 class="section">标题</h2> ... </section>
组件速查见 SKILL.md「组件速查」。 -->
<footer>{{FOOTER}}</footer>
</main>
</div>
<!-- [html-doc] 需要批注功能时:
1) 给要批注的 <table> 加 data-annotate="唯一名";或给任意区块加 data-note-id="唯一名"
2) 把 assets/annotate.js 的全部内容内联到下面的 <script> 中并取消注释 -->
<!-- <script>/* INLINE assets/annotate.js */</script> -->
</body>
</html>

24
.gitattributes vendored Normal file
View File

@@ -0,0 +1,24 @@
# Changelog 视图由多个 fragment 生成;并行分支合并时保留双方新增行。
.cursor/changelog/changelog-full.md text eol=lf merge=union
.cursor/changelog/changelog-headlines.md text eol=lf merge=union
.cursor/changelog/changelog-recent.md text eol=lf merge=union
.cursor/changelog/changelog-by-author.md text eol=lf merge=union
# 跨平台脚本、配置、文档和 Godot 文本资源统一使用 LF。
*.ps1 text eol=lf
*.sh text eol=lf
*.py text eol=lf
*.json text eol=lf
*.md text eol=lf
*.mdc text eol=lf
*.yml text eol=lf
*.yaml text eol=lf
*.toml text eol=lf
*.cfg text eol=lf
*.gd text eol=lf
*.gdshader text eol=lf
*.tscn text eol=lf
*.tres text eol=lf
*.uid text eol=lf
*.import text eol=lf
project.godot text eol=lf

29
.gitignore vendored
View File

@@ -4,6 +4,7 @@
# Hook 运行态标记(仅影响本地会话)
.cursor/changelog/.changelog-ack
.cursor/changelog/.session-start
# cursor-init 运行时临时产物
.cursor/_init-backup/
@@ -12,6 +13,7 @@
# 注意:不要忽略 .cursor/.init-done
# 本模板仓库自身应当 **不包含** 此文件(让 clone 者首次会话触发 init
# 下游项目 init 完成后需要 commit 它
!.cursor/.init-done
# === OS ===
# Windows
@@ -46,6 +48,33 @@ $RECYCLE.BIN/
.idea/
*.iml
# === Python ===
__pycache__/
*.py[cod]
*$py.class
.pytest_cache/
.mypy_cache/
.ruff_cache/
.coverage
htmlcov/
.venv/
venv/
# === Godot ===
# Godot 4 的编辑器缓存和导入缓存(可安全重新生成)
.godot/
# 常见的根级构建/导出目录
/build/
/dist/
/out/
/export/
/exports/
# Godot 资源 UID 与导入元数据必须提交,不要添加 .uid 或 *.import 忽略规则
!*.uid
!*.import
# === Custom ===
# 本模板仓库自身的临时文件
*.log