diff --git a/.cursor/README.html b/.cursor/README.html new file mode 100644 index 0000000..82bf4e8 --- /dev/null +++ b/.cursor/README.html @@ -0,0 +1,214 @@ + + + + + + .cursor 协作指南 + + + +
+
+

.cursor 协作指南

+

项目级 Agent 记忆、计划和开发日志的基础设施。所有内容均以 .cursor/ 为根路径;初始化后的空模板不包含任何项目历史。

+ fragment changelog + deterministic views + UTF-8 · LF +
+ + + +
+
+

目录结构

+
    +
  • .cursor/changelog/entries/:开发日志 fragment 的唯一数据源。
  • +
  • .cursor/changelog/changelog-*.md:由生成器确定性生成的只读视图。
  • +
  • .cursor/changelog/tools/:生成、迁移、合并导入和 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
  • +
+
python .cursor/changelog/tools/changelog_build.py
+python .cursor/changelog/tools/changelog_build.py --check
+
生成器要求 Python 3.10 或更高版本。
+
+ +
+

安装 Git hooks

+

Windows

+
powershell -ExecutionPolicy Bypass -File .cursor/changelog/tools/install-git-hooks.ps1
+

Linux / macOS

+
sh .cursor/changelog/tools/install-git-hooks.sh
+

pre-commit 会重建并暂存四个视图;post-merge 会在合并后重新生成视图。每个克隆只需安装一次。

+
+ +
+

导入旧格式日志

+

仅在需要把旧的聚合日志转换为 fragment 时使用:

+
powershell -ExecutionPolicy Bypass -File .cursor/changelog/tools/merge-import.ps1
+
sh .cursor/changelog/tools/merge-import.sh
+

也可将分支或 ref 作为参数传入。导入工具只添加尚不存在的 ID,随后重建并校验视图。

+
+ +
+

维护约定

+
    +
  • 生成视图只由 .cursor/changelog/tools/changelog_build.py 维护。
  • +
  • 新记录写入对应模板文件,不在模板中保留其他项目的数据。
  • +
  • 所有文本文件使用 UTF-8(无 BOM)和 LF 换行。
  • +
+
本页与 .cursor/README.md 语义一致。
+
+
+ + +
+ + diff --git a/.cursor/README.md b/.cursor/README.md new file mode 100644 index 0000000..bcc3614 --- /dev/null +++ b/.cursor/README.md @@ -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//.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 换行。 diff --git a/.cursor/changelog/changelog-by-author.md b/.cursor/changelog/changelog-by-author.md new file mode 100644 index 0000000..1b83b09 --- /dev/null +++ b/.cursor/changelog/changelog-by-author.md @@ -0,0 +1,4 @@ +# Dev Changelog — By Author + +按作者聚合的贡献概览,便于多人协作时区分提交来源。 +> 本文件由 `tools/changelog_build.py` 自动生成,请勿手改。 diff --git a/.cursor/changelog/changelog-full.md b/.cursor/changelog/changelog-full.md index 389f6d4..42854a5 100644 --- a/.cursor/changelog/changelog-full.md +++ b/.cursor/changelog/changelog-full.md @@ -1,5 +1,6 @@ # Dev Changelog — Full 完整的开发改动记录,按时间倒序排列。作为主动 RAG 的数据源,用户手动唤醒时读取。 +> 本文件由 `tools/changelog_build.py` 从 `entries/` 自动生成,请勿手改;新增/修改请编辑 `entries//.md` 后重跑生成器。 ## 记录 diff --git a/.cursor/changelog/changelog-headlines.md b/.cursor/changelog/changelog-headlines.md index b03e961..6165cc5 100644 --- a/.cursor/changelog/changelog-headlines.md +++ b/.cursor/changelog/changelog-headlines.md @@ -1,3 +1,4 @@ # Dev Changelog — Headlines 最近 ~50 次改动的一句话概要,按时间倒序排列。每次会话自动注入上下文。 +> 本文件由 `tools/changelog_build.py` 从 `entries/` 自动生成,请勿手改。 diff --git a/.cursor/changelog/changelog-recent.md b/.cursor/changelog/changelog-recent.md index 9883ef1..f1c4665 100644 --- a/.cursor/changelog/changelog-recent.md +++ b/.cursor/changelog/changelog-recent.md @@ -1,4 +1,5 @@ -# Dev Changelog — Recent +# Dev Changelog — Recent -最近 ~10 次改动的摘要记录,按时间倒序排列。 +最近 ~20 次改动的摘要记录,按时间倒序排列。 当 Agent 检测到当前任务与近期改动相关时自动读取。 +> 本文件由 `tools/changelog_build.py` 从 `entries/` 自动生成,请勿手改。 diff --git a/.cursor/changelog/entries/.gitkeep b/.cursor/changelog/entries/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/.cursor/changelog/entries/.gitkeep @@ -0,0 +1 @@ + diff --git a/.cursor/changelog/tools/changelog_build.py b/.cursor/changelog/tools/changelog_build.py new file mode 100644 index 0000000..c38b258 --- /dev/null +++ b/.cursor/changelog/tools/changelog_build.py @@ -0,0 +1,336 @@ +#!/usr/bin/env python3 +"""changelog_build.py — 从 fragment 源生成三层 changelog 视图。 + +设计目标(解决多人协作 + git merge 冲突): +- 唯一数据源是 ``.cursor/changelog/entries//.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*$", 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//.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()) diff --git a/.cursor/changelog/tools/git-hooks/post-merge b/.cursor/changelog/tools/git-hooks/post-merge new file mode 100644 index 0000000..00b8ead --- /dev/null +++ b/.cursor/changelog/tools/git-hooks/post-merge @@ -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 diff --git a/.cursor/changelog/tools/git-hooks/pre-commit b/.cursor/changelog/tools/git-hooks/pre-commit new file mode 100644 index 0000000..3bf4a83 --- /dev/null +++ b/.cursor/changelog/tools/git-hooks/pre-commit @@ -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 diff --git a/.cursor/changelog/tools/install-git-hooks.ps1 b/.cursor/changelog/tools/install-git-hooks.ps1 new file mode 100644 index 0000000..1d74636 --- /dev/null +++ b/.cursor/changelog/tools/install-git-hooks.ps1 @@ -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)." diff --git a/.cursor/changelog/tools/install-git-hooks.sh b/.cursor/changelog/tools/install-git-hooks.sh new file mode 100644 index 0000000..04c69fc --- /dev/null +++ b/.cursor/changelog/tools/install-git-hooks.sh @@ -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 合并后重建)。" diff --git a/.cursor/changelog/tools/merge-import.ps1 b/.cursor/changelog/tools/merge-import.ps1 new file mode 100644 index 0000000..1472970 --- /dev/null +++ b/.cursor/changelog/tools/merge-import.ps1 @@ -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" diff --git a/.cursor/changelog/tools/merge-import.sh b/.cursor/changelog/tools/merge-import.sh new file mode 100644 index 0000000..55771f7 --- /dev/null +++ b/.cursor/changelog/tools/merge-import.sh @@ -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" diff --git a/.cursor/changelog/tools/migrate_changelog.py b/.cursor/changelog/tools/migrate_changelog.py new file mode 100644 index 0000000..a511cb8 --- /dev/null +++ b/.cursor/changelog/tools/migrate_changelog.py @@ -0,0 +1,252 @@ +#!/usr/bin/env python3 +"""migrate_changelog.py — 一次性迁移:把旧的三层 changelog 文件拆成 fragment。 + +把现有 ``changelog-full.md`` 按 ``### [CL-...]`` 头切成一条一文件的 fragment, +写到 ``entries/legacy/.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, + "", + "", + l2, + "", + "", + 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()) diff --git a/.cursor/deferred/registry.md b/.cursor/deferred/registry.md index c592c87..18fe3b7 100644 --- a/.cursor/deferred/registry.md +++ b/.cursor/deferred/registry.md @@ -1,11 +1,24 @@ # Deferred Decisions Registry +记录已明确推迟、等待前置条件或需要后续复审的决策。 + ## Active Items -(暂无延期方案) + ## Completed / Cancelled Items -(暂无已完成或已废弃的方案) + diff --git a/.cursor/distillery/golden-rules.md b/.cursor/distillery/golden-rules.md index 9b0d868..04f6324 100644 --- a/.cursor/distillery/golden-rules.md +++ b/.cursor/distillery/golden-rules.md @@ -1,5 +1,13 @@ # Golden Rules -经过实践验证(权重 >= 5)的精炼认知,每次新会话自动注入。 +经过实践验证、达到采纳阈值的精炼认知。只保留跨任务稳定成立的规则。 - + diff --git a/.cursor/distillery/insights.md b/.cursor/distillery/insights.md index 4cf7343..6ff0243 100644 --- a/.cursor/distillery/insights.md +++ b/.cursor/distillery/insights.md @@ -1,8 +1,15 @@ # Problem Distillery — Insights -从已解决的顽固问题中蒸馏出的精炼方法论。 +从已解决的顽固问题中蒸馏出的可复用方法论。 - + --- -last_distill_date: 2026-04-21 +last_distill_date: diff --git a/.cursor/distillery/problems.md b/.cursor/distillery/problems.md index c27e8f4..3459382 100644 --- a/.cursor/distillery/problems.md +++ b/.cursor/distillery/problems.md @@ -1,5 +1,14 @@ # Problem Distillery — Problems -反复出现的顽固问题追踪记录,按时间倒序排列。 +反复出现、尚待稳定解决的顽固问题追踪记录,按时间倒序排列。 - + diff --git a/.cursor/hooks/README.md b/.cursor/hooks/README.md index 51f3c71..bd47d03 100644 --- a/.cursor/hooks/README.md +++ b/.cursor/hooks/README.md @@ -1,38 +1,61 @@ # Cursor Hooks 说明 -## 结构 +## 文件 -``` +```text hooks/ -├── run-hook.ps1 # Windows dispatcher(读取 local-env.json 后分发) -├── session-init.ps1 # sessionStart hook(PowerShell 版) -├── session-init.sh # sessionStart hook(bash 版) -├── check-changelog.ps1 # stop hook(PowerShell 版) -├── check-changelog.sh # stop hook(bash 版) -└── README.md # 本文件 +├── run-hook.ps1 # Windows dispatcher;读取 local-env.json 并校验 JSON 输出 +├── session-init.ps1 # sessionStart(PowerShell) +├── session-init.sh # sessionStart(Bash) +├── check-changelog.ps1 # stop / changelog guard(PowerShell) +├── check-changelog.sh # stop / changelog guard(Bash) +└── 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 对象;检查失败默认不阻断。 diff --git a/.cursor/hooks/check-changelog.ps1 b/.cursor/hooks/check-changelog.ps1 index 9243fc2..d30c712 100644 --- a/.cursor/hooks/check-changelog.ps1 +++ b/.cursor/hooks/check-changelog.ps1 @@ -1,105 +1,124 @@ -# Changelog sync guard — 统一 stop hook -# -# 用确定性逻辑检查: -# 1. 源文件是否比 changelog 更新(mtime 比较) -# 2. stdin 中的 agent 上下文是否显示有源文件编辑操作 -# -# 静默条件(不触发提醒): -# 1. 环境变量 CURSOR_SKIP_CHANGELOG 被设置(sessionStart 在非 agent 模式设置) -# 2. stdin JSON 中 composer_mode 不是 "agent"(如 debug/ask/edit 模式) -# 3. changelog 文件不存在 -# 4. .changelog-ack 标记文件存在且足够新(本会话已确认过 changelog 状态) -# 5. 没有源文件比 changelog 更新 -# 6. stdin 上下文中没有文件编辑操作的证据(防止跨会话残留 mtime 误触发) +# Changelog sync guard: compare Godot source/config mtimes against this session and changelog. +$utf8 = New-Object System.Text.UTF8Encoding($false) +[Console]::InputEncoding = $utf8 +[Console]::OutputEncoding = $utf8 -$input = [Console]::In.ReadToEnd() +function Write-EmptyHookResult { + [Console]::Out.WriteLine('{}') +} -# === 豁免检查 1:环境变量跳过标志 === +$null = [Console]::In.ReadToEnd() + +# sessionStart sets this variable outside agent mode. if ($env:CURSOR_SKIP_CHANGELOG) { - Write-Output '{}' + Write-EmptyHookResult exit 0 } -# === 豁免检查 2:从 stdin 解析 composer_mode === -try { - $data = $input | ConvertFrom-Json - $mode = $data.composer_mode - if ($mode -and $mode -ne "agent") { - Write-Output '{}' - exit 0 +$cursorDir = Split-Path -Parent $PSScriptRoot +$repoRoot = Split-Path -Parent $cursorDir +$changelog = Join-Path $cursorDir 'changelog\changelog-headlines.md' +$ackFile = Join-Path $cursorDir 'changelog\.changelog-ack' +$sessionMarker = Join-Path $cursorDir 'changelog\.session-start' + +if (-not (Test-Path -LiteralPath $changelog -PathType Leaf)) { + Write-EmptyHookResult + exit 0 +} + +$changelogMtime = (Get-Item -LiteralPath $changelog).LastWriteTimeUtc +if ((Test-Path -LiteralPath $ackFile -PathType Leaf) -and + (Get-Item -LiteralPath $ackFile).LastWriteTimeUtc -ge $changelogMtime) { + Write-EmptyHookResult + exit 0 +} + +$comparisonMtime = $changelogMtime +if ((Test-Path -LiteralPath $sessionMarker -PathType Leaf)) { + $sessionMtime = (Get-Item -LiteralPath $sessionMarker).LastWriteTimeUtc + if ($sessionMtime -gt $comparisonMtime) { + $comparisonMtime = $sessionMtime } -} catch { - # JSON 解析失败,继续后续检查 } -# === 豁免检查 3:从 stdin 文本匹配 debug 上下文关键词 === -# stop hook 的 $ARGUMENTS 可能包含对话/工具上下文,检测 debug 相关信号 -if ($input -match '"mode"\s*:\s*"debug"' -or - $input -match 'debug[\s_-]?mode' -or - $input -match 'Debug Mode') { - Write-Output '{}' - exit 0 -} - -$changelog = ".cursor\changelog\changelog-headlines.md" -$srcDir = "art-agent" - -# === 豁免检查 4:changelog 文件不存在 === -if (-not (Test-Path $changelog)) { - Write-Output '{}' - exit 0 -} - -$clMtime = (Get-Item $changelog).LastWriteTime - -# === 豁免检查 5:ack 标记文件足够新 === -$ackFile = ".cursor\changelog\.changelog-ack" -if ((Test-Path $ackFile) -and (Get-Item $ackFile).LastWriteTime -ge $clMtime) { - Write-Output '{}' - exit 0 -} - -# === 豁免检查 6:stdin 中无文件编辑证据 === -# 防止跨会话残留 mtime 差异导致误触发:如果 ack 文件不存在(或过旧), -# 但 stdin 上下文中也没有任何文件写入/编辑操作的痕迹,说明本次会话 -# 没有进行代码改动,不应触发提醒。 -$hasEditEvidence = ( - $input -match 'StrReplace|Write\s*tool|edit_file|file_write|write_to_file' -or - $input -match 'Created file|Modified file|Wrote contents' -or - $input -match '"tool"\s*:\s*"(str_replace|write|edit)"' +$includedNames = @('project.godot') +$includedExtensions = @( + '.gd', + '.tscn', + '.tres', + '.res', + '.gdshader', + '.cfg', + '.json', + '.cs', + '.csproj', + '.py', + '.ps1', + '.sh' +) +$excludedDirectoryNames = @( + '.git', + '.cursor', + '.godot', + 'node_modules', + '__pycache__', + 'venv', + '.venv', + 'dist', + 'build', + 'out', + 'bin', + 'obj', + 'export', + 'exports', + 'coverage', + 'tmp', + 'temp' ) -if (-not $hasEditEvidence) { - Write-Output '{}' - exit 0 -} - -# === 核心检查:是否有源文件比 changelog 更新 === -$extensions = @("*.py", "*.tsx", "*.ts", "*.css") -$excludeDirs = @("node_modules", ".next", "__pycache__", "venv") $newerFile = $null -foreach ($ext in $extensions) { - $files = Get-ChildItem -Path $srcDir -Filter $ext -Recurse -ErrorAction SilentlyContinue | - Where-Object { - $skip = $false - foreach ($ex in $excludeDirs) { - if ($_.FullName -like "*\$ex\*") { $skip = $true; break } - } - -not $skip -and $_.LastWriteTime -gt $clMtime - } | - Select-Object -First 1 +$rootItem = Get-Item -LiteralPath $repoRoot +$directories = New-Object 'System.Collections.Generic.Stack[System.IO.DirectoryInfo]' +$directories.Push($rootItem) - if ($files) { - $newerFile = $files.Name - break +while ($directories.Count -gt 0 -and -not $newerFile) { + $directory = $directories.Pop() + try { + $entries = Get-ChildItem -LiteralPath $directory.FullName -Force -ErrorAction Stop + } catch { + continue + } + + foreach ($entry in $entries) { + if ($entry.PSIsContainer) { + $isReparsePoint = ($entry.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0 + if (-not $isReparsePoint -and $excludedDirectoryNames -notcontains $entry.Name) { + $directories.Push($entry) + } + continue + } + + $isIncluded = ($includedNames -contains $entry.Name) -or + ($includedExtensions -contains $entry.Extension) + if ($isIncluded -and $entry.LastWriteTimeUtc -gt $comparisonMtime) { + $newerFile = $entry + break + } } } if ($newerFile) { - $msg = "[Hook] Source file updated (e.g. $newerFile) but changelog not synced. Run dev-changelog Skill operation A NOW to write all three changelog layers." - $json = '{"followup_message":"' + $msg.Replace('"','\"') + '"}' - Write-Output $json + $rootPrefix = $repoRoot.TrimEnd('\', '/') + [System.IO.Path]::DirectorySeparatorChar + $relativePath = if ($newerFile.FullName.StartsWith($rootPrefix, [System.StringComparison]::OrdinalIgnoreCase)) { + $newerFile.FullName.Substring($rootPrefix.Length) + } else { + $newerFile.Name + } + $message = "[Hook] Godot source/config is newer than the changelog (e.g. $relativePath). Run dev-changelog Skill operation A now: add a fragment under .cursor/changelog/entries/ and rebuild the changelog views." + $result = [ordered]@{ followup_message = $message } + [Console]::Out.WriteLine(($result | ConvertTo-Json -Compress -Depth 3)) } else { - Write-Output '{}' + Write-EmptyHookResult } + exit 0 diff --git a/.cursor/hooks/check-changelog.sh b/.cursor/hooks/check-changelog.sh index 5265952..f7c4545 100644 --- a/.cursor/hooks/check-changelog.sh +++ b/.cursor/hooks/check-changelog.sh @@ -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 +cat >/dev/null + +if [ -n "${CURSOR_SKIP_CHANGELOG:-}" ]; then + emit_empty + exit 0 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 +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" -# === 豁免检查 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" - -# === 豁免检查 4:changelog 文件不存在 === if [ ! -f "$changelog" ]; then - echo '{}'; exit 0 + emit_empty + exit 0 fi -clMtime=$(stat -c %Y "$changelog" 2>/dev/null || stat -f %m "$changelog" 2>/dev/null) +get_mtime() { + stat -c %Y "$1" 2>/dev/null || stat -f %m "$1" 2>/dev/null +} -# === 豁免检查 5:ack 标记文件足够新 === -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 +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 -# === 豁免检查 6:stdin 中无文件编辑证据 === -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 +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 -# === 核心检查:是否有源文件比 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) +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 [ -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\"}" +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 - echo '{}' + emit_empty fi + exit 0 diff --git a/.cursor/hooks/run-hook.ps1 b/.cursor/hooks/run-hook.ps1 index a6b726b..4ac2504 100644 --- a/.cursor/hooks/run-hook.ps1 +++ b/.cursor/hooks/run-hook.ps1 @@ -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 -# 示例: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 -if (Test-Path $localEnvPath) { - try { - $config = Get-Content $localEnvPath -Raw | ConvertFrom-Json - if ($config.shell) { $shell = $config.shell } - } catch {} +function Write-EmptyHookResult { + [Console]::Out.WriteLine('{}') } -$hookDir = ".cursor\hooks" -$input = [Console]::In.ReadToEnd() +$inputPayload = [Console]::In.ReadToEnd() -if ($shell -eq "powershell") { - $scriptPath = Join-Path $hookDir "$HookName.ps1" - if (Test-Path $scriptPath) { - $input | powershell -ExecutionPolicy Bypass -File $scriptPath - } else { - Write-Output '{}' +try { + if ([string]::IsNullOrWhiteSpace($HookName) -or $HookName -notmatch '^[A-Za-z0-9][A-Za-z0-9._-]*$') { + throw "Invalid hook name." } -} else { - $scriptPath = Join-Path $hookDir "$HookName.sh" - if (Test-Path $scriptPath) { - $input | bash $scriptPath - } else { - Write-Output '{}' + + $cursorDir = Split-Path -Parent $PSScriptRoot + $repoRoot = Split-Path -Parent $cursorDir + $localEnvPath = Join-Path $cursorDir 'local-env.json' + $shell = 'powershell' + + 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() + } + } 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 diff --git a/.cursor/hooks/session-init.ps1 b/.cursor/hooks/session-init.ps1 index de54553..a35df45 100644 --- a/.cursor/hooks/session-init.ps1 +++ b/.cursor/hooks/session-init.ps1 @@ -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 - $mode = $data.composer_mode + 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 diff --git a/.cursor/hooks/session-init.sh b/.cursor/hooks/session-init.sh index 34bc498..dcb3f39 100644 --- a/.cursor/hooks/session-init.sh +++ b/.cursor/hooks/session-init.sh @@ -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 "") +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 -rm -f ".cursor/changelog/.changelog-ack" +if [ -n "$python_cmd" ]; then + result=$( + printf '%s' "$input" | "$python_cmd" -c ' +import json +import sys -if [ -n "$mode" ] && [ "$mode" != "agent" ]; then - echo "{\"env\":{\"CURSOR_SKIP_CHANGELOG\":\"1\",\"CURSOR_COMPOSER_MODE\":\"$mode\"}}" +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 - echo "{}" + printf '{}\n' fi + exit 0 diff --git a/.cursor/local-env.example.json b/.cursor/local-env.example.json index 963a16f..2d93f74 100644 --- a/.cursor/local-env.example.json +++ b/.cursor/local-env.example.json @@ -1,5 +1,9 @@ { - "_comment": "设备绑定的本地环境配置。复制为 local-env.json 并修改为本机实际值。", - "nodejs_path": "C:\\Users\\\\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": "", + "_changelog_author_comment": "用于 changelog 条目的作者标识,例如团队账号或个人 handle。", + "godot4_path": "", + "_godot4_path_comment": "填写本机 Godot 4 可执行文件路径或可从 PATH 解析的命令名;不要把个人绝对路径写入模板。" } diff --git a/.cursor/pitfalls/pitfalls.md b/.cursor/pitfalls/pitfalls.md index faaebf1..99ec137 100644 --- a/.cursor/pitfalls/pitfalls.md +++ b/.cursor/pitfalls/pitfalls.md @@ -1,6 +1,15 @@ # Pitfall Journal -开发过程中踩过的坑,按时间倒序排列。 -Agent 进入 Debug mode 或遇到运行时错误时自动检索匹配。 +开发过程中踩过的坑,按时间倒序排列。Agent 进入调试或遇到运行时错误时可检索本文件。 + + diff --git a/.cursor/plans/.gitkeep b/.cursor/plans/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/.cursor/plans/.gitkeep @@ -0,0 +1 @@ + diff --git a/.cursor/profile/project-profile-log.md b/.cursor/profile/project-profile-log.md index 5796dbe..e86e487 100644 --- a/.cursor/profile/project-profile-log.md +++ b/.cursor/profile/project-profile-log.md @@ -1,5 +1,15 @@ # Project Profile Log -详细记录每次项目画像更新的完整上下文,按时间正序追加。 +详细记录项目画像的新增、演进和删除上下文,按时间正序追加。 ## 记录 + + diff --git a/.cursor/profile/project-profile.md b/.cursor/profile/project-profile.md index cedb7e9..60f7cdc 100644 --- a/.cursor/profile/project-profile.md +++ b/.cursor/profile/project-profile.md @@ -1,9 +1,19 @@ # Project Profile +记录当前项目中稳定、可复用且已确认的事实。详细演进过程写入 `.cursor/profile/project-profile-log.md`。 + ## 项目定位 + + ## 技术栈与架构 + + ## 设计约定 + + ## 产品方向 + + diff --git a/.cursor/rules/common/baserule.mdc b/.cursor/rules/common/baserule.mdc index b017d8f..993222a 100644 --- a/.cursor/rules/common/baserule.mdc +++ b/.cursor/rules/common/baserule.mdc @@ -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 的临时文件后再用文件工具读取。 diff --git a/.cursor/rules/common/changelog-recall.mdc b/.cursor/rules/common/changelog-recall.mdc index 951433b..9535024 100644 --- a/.cursor/rules/common/changelog-recall.mdc +++ b/.cursor/rules/common/changelog-recall.mdc @@ -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//.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 写入,不越权修改。 diff --git a/.cursor/rules/common/cursor-init-recall.mdc b/.cursor/rules/common/cursor-init-recall.mdc index 5e711e1..91b5715 100644 --- a/.cursor/rules/common/cursor-init-recall.mdc +++ b/.cursor/rules/common/cursor-init-recall.mdc @@ -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 清单并取得确认。 +- 分类不清的文件逐项询问,不猜测归属。 +- 流程保持幂等;重复执行不应破坏已经正确的状态。 diff --git a/.cursor/rules/common/deferred-recall.mdc b/.cursor/rules/common/deferred-recall.mdc index 1f66c76..6383534 100644 --- a/.cursor/rules/common/deferred-recall.mdc +++ b/.cursor/rules/common/deferred-recall.mdc @@ -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 不存在时只报告,不自行发明记录格式。 diff --git a/.cursor/rules/common/dependency-governance.mdc b/.cursor/rules/common/dependency-governance.mdc new file mode 100644 index 0000000..6de07f9 --- /dev/null +++ b/.cursor/rules/common/dependency-governance.mdc @@ -0,0 +1,20 @@ +--- +description: 约束外部依赖、插件与第三方资源的引入和升级 +alwaysApply: true +--- + +# 依赖与外部资源治理 + +引入、下载、安装或升级任何外部依赖前,必须先获得用户确认,包括: + +- 引擎插件、addon、扩展和原生库; +- npm、pip 等包管理依赖及锁文件变更; +- Asset Library 或网络来源的素材、字体、音频、模型和模板; +- 会改变构建、导出或运行环境的工具链组件。 + +确认前说明:名称、用途、目标版本、来源、许可、维护状态、影响范围,以及工程内是否已有替代方案。 + +- 来源或许可不明确时默认不引入。 +- 优先复用工程已有能力;不得为便利而复制功能重叠的依赖。 +- 升级前检查项目声明的运行时兼容范围和变更说明。 +- 用户拒绝或暂未确认时,提供无新增依赖的方案,不静默安装。 diff --git a/.cursor/rules/common/distillery-recall.mdc b/.cursor/rules/common/distillery-recall.mdc index f47815f..0992cad 100644 --- a/.cursor/rules/common/distillery-recall.mdc +++ b/.cursor/rules/common/distillery-recall.mdc @@ -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 缺失时报告缺失,不自行创造结构。 diff --git a/.cursor/rules/common/epee-orchestrator.mdc b/.cursor/rules/common/epee-orchestrator.mdc index 1162057..225e082 100644 --- a/.cursor/rules/common/epee-orchestrator.mdc +++ b/.cursor/rules/common/epee-orchestrator.mdc @@ -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 时须预置空的自迭代日志章节 +用户限制可修改范围时不得越权;只报告待同步项。 diff --git a/.cursor/rules/common/pitfall-recall.mdc b/.cursor/rules/common/pitfall-recall.mdc index da8fb39..67daa68 100644 --- a/.cursor/rules/common/pitfall-recall.mdc +++ b/.cursor/rules/common/pitfall-recall.mdc @@ -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 缺失时,不越权写入,只报告待记录事项。 diff --git a/.cursor/rules/common/profile-recall.mdc b/.cursor/rules/common/profile-recall.mdc index 6dec597..b551c51 100644 --- a/.cursor/rules/common/profile-recall.mdc +++ b/.cursor/rules/common/profile-recall.mdc @@ -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. 提取该条目的锚点 ID(HTML 注释中的 `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 不存在时只报告缺失,不自行创建不兼容格式。 diff --git a/.cursor/rules/project/godot-architecture.mdc b/.cursor/rules/project/godot-architecture.mdc new file mode 100644 index 0000000..08b919f --- /dev/null +++ b/.cursor/rules/project/godot-architecture.mdc @@ -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`。 diff --git a/.cursor/rules/project/godot-assets.mdc b/.cursor/rules/project/godot-assets.mdc new file mode 100644 index 0000000..4abdf35 --- /dev/null +++ b/.cursor/rules/project/godot-assets.mdc @@ -0,0 +1,26 @@ +--- +description: Godot 素材引用、导入一致性与可替换占位规范 +globs: "**/*.{gd,tscn,tres,res}" +alwaysApply: false +--- + +# Godot 素材与占位 + +## 正式素材 + +- 面向用户的成品视觉应使用项目批准的正式素材;程序绘制只用于确属程序化的视觉、调试或明确的临时占位。 +- 沿用项目现有素材目录、命名、尺寸基准和导入预设,不假设固定路径或分辨率。 +- 文件命名保持稳定、可搜索;移动或重命名后检查场景、资源和脚本引用。 +- 纹理过滤、mipmap、压缩、循环和采样等导入设置按素材用途确定,同类素材保持一致。 +- 逻辑层只引用稳定的资源标识或数据,不直接依赖表现素材。 + +## 缺失素材占位 + +正式素材缺失时可以占位,但必须满足: + +- 视觉上明显区别于成品,并显示或记录可搜索的 asset key; +- 在代码或清单中标注 `PLACEHOLDER`,注明期望用途和替换条件; +- 占位节点尺寸、锚点和接口与目标素材兼容,替换时不要求改业务逻辑; +- 项目已有统一占位生成器或缺失素材清单时必须复用;没有时采用最小可删除实现,不额外建立复杂系统。 + +提交或交付前搜索未解决的 `PLACEHOLDER`,明确哪些仍被允许保留。 diff --git a/.cursor/rules/project/godot-data-systems.mdc b/.cursor/rules/project/godot-data-systems.mdc new file mode 100644 index 0000000..d911001 --- /dev/null +++ b/.cursor/rules/project/godot-data-systems.mdc @@ -0,0 +1,28 @@ +--- +description: Godot 数据驱动内容、数据访问、校验与存档兼容约定 +globs: "**/*.{gd,tres,res,csv,json}" +alwaysApply: false +--- + +# Godot 数据系统 + +## 数据驱动边界 + +- 需要由策划或内容人员独立调整的参数、实体定义和效果配置,应放入项目采用的数据源,不散落为脚本字面量。 +- 先复用项目已有的 `Resource`、CSV、JSON 或其他数据管线;项目尚未采用数据驱动时,根据实际复用需求选择最小方案,不强制新增格式或仓库层。 +- 技术常量、枚举、协议版本和与内容无关的安全默认值可以留在代码中,并用名称或注释说明用途。 +- 不明确某个数值属于内容还是技术约束时,先查现有数据定义和调用方。 + +## 加载与访问 + +- 为数据定义稳定 ID、字段类型、必填项和默认值;加载边界统一做格式与引用校验。 +- 解析失败应包含数据源、记录 ID 和字段名;可恢复问题使用安全默认值并报告,不可恢复问题尽早失败。 +- 若项目已有 Store、Repository 或缓存层,公开查询入口统一经过该层,并提供测试可用的清理或重载能力。 +- 调用方不应重复解析同一数据源,也不应依赖数据文件中的偶然顺序。 +- 新增可配置行为时优先扩展既有注册或组合机制,避免在多个调用点堆叠同类条件分支。 + +## 存档与兼容 + +- 持久化数据携带明确版本;读取时对缺失字段提供默认值,并为不兼容结构提供显式转换。 +- 只保存恢复状态所需的数据,不序列化场景节点引用或临时缓存。 +- 数据结构变化时同时更新加载、保存、校验和回归测试。 diff --git a/.cursor/rules/project/godot-gdscript-style.mdc b/.cursor/rules/project/godot-gdscript-style.mdc new file mode 100644 index 0000000..95f7742 --- /dev/null +++ b/.cursor/rules/project/godot-gdscript-style.mdc @@ -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`,不要用断言处理正常用户输入。 diff --git a/.cursor/rules/project/godot-scene-conventions.mdc b/.cursor/rules/project/godot-scene-conventions.mdc new file mode 100644 index 0000000..3ed450f --- /dev/null +++ b/.cursor/rules/project/godot-scene-conventions.mdc @@ -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` 只做每帧必需工作;可事件驱动的更新不要轮询。 +- 高频实例化是否使用对象池由性能测量决定,不预先为普通节点增加池化复杂度。 + +## 用户文本 + +用户可见文本集中在表现层,并接入项目已有本地化方案;不要在深层逻辑中硬编码最终文案。 diff --git a/.cursor/rules/project/godot-testing.mdc b/.cursor/rules/project/godot-testing.mdc new file mode 100644 index 0000000..8867328 --- /dev/null +++ b/.cursor/rules/project/godot-testing.mdc @@ -0,0 +1,29 @@ +--- +description: Godot 测试脚本的 headless、隔离、确定性与退出码规范 +globs: "**/*test*.gd" +alwaysApply: false +--- + +# Godot 测试 + +## 运行原则 + +- 领域逻辑应能在无窗口、无 UI 和无渲染依赖的环境中验证。 +- 运行前按项目声明确定 Godot 版本、可执行文件和测试入口;不假设固定目录、场景名或脚本命名。 +- 项目已有测试框架或统一入口时必须复用。没有时采用最小可执行入口;引入第三方框架前遵守依赖确认规则。 +- 自动化测试成功返回退出码 0,失败返回非零;不得仅依赖日志文本判断结果。 +- 性能与超时断言应测量被测逻辑本身,避免输出管道、编辑器启动和资源导入污染计时。 + +## 用例约束 + +- 每个用例独立设置并清理状态,不依赖执行顺序或前一个用例留下的 autoload、缓存、随机种子和文件。 +- 随机、时间和异步流程使用可控种子、可注入时钟或明确超时,避免无限等待。 +- 优先断言公开行为、信号参数和持久化结果,不绑定私有实现细节。 +- 纯逻辑用例不实例化 UI、3D 或特效场景;只有验证集成边界时才加载必要场景。 +- 失败信息包含期望值、实际值和关键实体标识,便于在 headless 日志中定位。 + +## 变更要求 + +- 修复缺陷时先补可复现用例,或说明为何无法自动化。 +- 新增逻辑系统时覆盖正常路径、边界输入、失败路径和状态重置。 +- 数据 schema、存档版本、信号契约或项目配置变化时同步更新相关回归测试。 diff --git a/.cursor/skills/cursor-init/SKILL.md b/.cursor/skills/cursor-init/SKILL.md index fcbbe33..5462281 100644 --- a/.cursor/skills/cursor-init/SKILL.md +++ b/.cursor/skills/cursor-init/SKILL.md @@ -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 的阶段 0–7。 +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/.mdc` | -| `.mdc` 且内容是当前新项目专属规则 | 项目 Rule | `.cursor/rules/project/.mdc` | -| 包含 `SKILL.md` 的目录 | Skill(无论通用/项目专属) | `.cursor/skills//` | -| 看不出归属 | 询问用户 | — | -| 明显是临时/垃圾文件(`*.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. 解析现有条目(每条 `### ` 块) -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)的精炼认知,每次新会话自动注入。 - - -``` - -### profile/project-profile.md - -```markdown -# Project Profile - -## 项目定位 - -## 技术栈与架构 - -## 设计约定 - -## 产品方向 -``` - -### profile/project-profile-log.md - -```markdown -# Project Profile Log - -详细记录每次项目画像更新的完整上下文,按时间正序追加。 - -## 记录 -``` - -### .init-done - -阶段 7 写入的 sentinel,YAML 格式,字段固定: - -```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.lock:lib 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 组 skill;C 组整个 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` 只在所有验证通过后存在。 diff --git a/.cursor/skills/cursor-init/reference.md b/.cursor/skills/cursor-init/reference.md new file mode 100644 index 0000000..8ea556e --- /dev/null +++ b/.cursor/skills/cursor-init/reference.md @@ -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)的精炼认知,每次新会话自动注入。 + + +``` + +### 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//.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 后扫描结果变化时应重新生成清单并再次确认。 diff --git a/.cursor/skills/dev-changelog/SKILL.md b/.cursor/skills/dev-changelog/SKILL.md index f31c4a9..768bd3d 100644 --- a/.cursor/skills/dev-changelog/SKILL.md +++ b/.cursor/skills/dev-changelog/SKILL.md @@ -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//.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-`(如 `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//`、② 文件名 / ID 后缀、 +③ frontmatter `author` 字段。视图层 L3 行前缀 `@`、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.md(L2)中 Grep 该 ID - - 找到 → 读取该条目的 3-5 行摘要,通常足以消除歧义 - - 未找到(已滚出 L2 窗口)→ 进入步骤 3 -3. 在 changelog-full.md(L1)中 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 +--- + + +一句话描述改动内容(≤80 字,进 headlines) + + +做了什么 + 为什么(3-5 行摘要,进 recent;当本条进入最近 20 时被展示) + + +- **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 之外的三段用 `` 分隔 ## 操作 A:记录写入(核心流程) ### 触发条件 -Agent 完成了一个涉及**代码或配置文件实质性改动**的任务后,自动触发。 +Agent 完成涉及**代码或配置文件实质性改动**的任务后自动触发。 -以下情况**不触发**: -- 纯对话讨论、方案设计、问题解答(无文件改动) -- 只读操作(查看文件、搜索代码) -- 只改动了 `.cursor/` 目录下的基础设施文件(如画像、延期方案、changelog 自身) +**不触发**:纯对话/方案/答疑(无文件改动)、只读操作、只改 `.cursor/` 下基础设施文件 +(画像、延期、changelog 自身、hooks、tools 等)。 ### 写入流程 ``` -1. 生成锚点 ID:CL-YYYYMMDD-HHMM(检查是否与已有 ID 冲突,冲突则追加字母后缀) +1. 确定作者 handle(见上「作者标识」三步顺序) -2. 从刚完成的任务中提取以下信息: - - 做了什么(what):一句话概括 - - 为什么这样做(why):动机和背景 - - 改了哪里(where):受影响的文件/模块列表 - - 关键决策(decisions):如果有方案选择,记录选了什么、放弃了什么 - - 注意事项(notes):后续可能受影响的地方、已知限制等 +2. 生成锚点 ID:CL-YYYYMMDD-HHMM- + - 检查 entries/ 下是否已存在同 ID(含其他作者目录),冲突则 HHMM 后加字母后缀 -3. 生成三层内容(共享同一个锚点 ID): - - L1 完整条目(包含以上全部信息) - - L2 摘要条目(what + why + where,3-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//.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`)。 ## 操作 B:L2 自动触发读取 -### 触发条件 - -由 `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。 ## 操作 C:L1 手动检索 -### 触发条件 +用户要求回顾完整记录时,读取 `changelog-full.md` 并按时间 / 模块 / tags 过滤展示。 -用户主动要求回顾完整改动记录时触发。典型话语: -- "回顾一下最近的改动" -- "XX 模块之前改过什么" -- "查看开发日志" -- "changelog" - -### 流程 +## 逐级上溯机制 +L3 信息不足时(语义模糊),按 ID 逐级 Grep: ``` -1. 读取 changelog-full.md -2. 根据用户需求过滤: - - 按时间范围 - - 按模块/文件 - - 按 tags -3. 展示匹配的条目摘要表,用户可以进一步查看某条的完整内容 +1. 从 L3 提取锚点 ID +2. Grep changelog-recent.md(L2)→ 命中读该条 +3. 仍不足 → Grep changelog-full.md(L1)拿行号,Read 该行 ±20 行 + (也可直接 Read 对应 fragment:entries/*/.md) ``` +原则:按需触发、精准读取、一次 1-3 条、不全文读 L1。 ## 操作 D:记录管理 -用户可以对已有记录进行管理: +- **修改**:编辑对应 `entries/*/.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 观感)。 diff --git a/.cursor/skills/epee-orchestrator/SKILL.md b/.cursor/skills/epee-orchestrator/SKILL.md index b239b5c..33cec8d 100644 --- a/.cursor/skills/epee-orchestrator/SKILL.md +++ b/.cursor/skills/epee-orchestrator/SKILL.md @@ -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 同步作为独立检查项。 diff --git a/.cursor/skills/epee-orchestrator/registry.md b/.cursor/skills/epee-orchestrator/registry.md index 1a0e537..fd3c0d0 100644 --- a/.cursor/skills/epee-orchestrator/registry.md +++ b/.cursor/skills/epee-orchestrator/registry.md @@ -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//.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.md、insights.md、golden-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 diff --git a/.cursor/skills/html-doc/SKILL.md b/.cursor/skills/html-doc/SKILL.md new file mode 100644 index 0000000..b5fbec9 --- /dev/null +++ b/.cursor/skills/html-doc/SKILL.md @@ -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/徽标/目录/代码块) | 内联进 ` + + +
+ + +
+
+

{{TITLE}}

+

{{SUBTITLE}}

+
+ + + +
{{FOOTER}}
+
+
+ + + + + diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..f3ea5f1 --- /dev/null +++ b/.gitattributes @@ -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 diff --git a/.gitignore b/.gitignore index abc6607..92e4eb1 100644 --- a/.gitignore +++ b/.gitignore @@ -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