关于 regression-notes
用于将决策记录与回归测试配套维护的技能,帮助后续修改保留决策缘由及受保护的行为。它提供创建、搜索、验证和归档记录的脚本,并可安装仓库验证关卡。
主要功能
- 01将决策记录绑定到测试
- 02创建记录及管理其生命周期
- 03搜索并验证记录目录
- 04可安装的 pre-commit 验证关卡
- 05区分不同修改类型
适用场景
- 01记录必须保留某项 Bug 修复的原因
- 02用回归测试固定变更后的行为
- 03更新或归档已被取代的决策
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ meimingqi222/ my-agent-skills --skill regression-notes安装后在 Claude Code 中输入/regression-notes确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/gate-and-format.mdscripts/new-note.pyverify-notes.pytemplates/bug-fix.mdgeneric.mdtests/test_date_timezone.pytest_install.pytest_reports.pytest_scale.pytest_verify_notes.py详细介绍
回归记录
版本 0.7.0
如果没有 python3(原版 Windows),请使用 python 或 py -3。
这是 DeepSeek Harness Agent Note 循环的轻量移植:如果某项更改的原因可能被后续更改遗忘,就应在同一次更改中提交一条记录(原因)+ 一个回归测试(锁定行为)。记录写明决策;测试固定行为。类别取决于决策类型:bug-fix 表示契约遭到破坏,spec-change 表示有意更改了需求,feature/architecture 表示新设计。
何时编写记录
记录是例外,测试才是默认做法。每次编辑记录所引用的文件前,都会先阅读该记录,因此每条记录都会让未来的每次更改变得稍微费心;每个 Bug 都有一条记录的代码树,会让热文件的查找结果多达数百条,没人会去看。请按每次更改分别判断:
| 更改…… | 编写 |
|---|---|
| 在同一次未提交的更改中引入并修复 | 无——从未影响到任何人 |
| 从代码和测试中一目了然 | 一个回归测试(优先在现有测试中多加一个断言,而不是新建文件)和一条清晰的提交说明 |
| 修复处的原因能用一行说明 | 该测试,以及修复处的代码注释——注释正好位于最容易让人忍不住修改的地方 |
| 是后续更改可能会撤销的决策:修复后的代码看起来多余或有误、某个诱人的替代方案被否决、做出了权衡(例如以性能换正确性),或更改涉及多个文件或共享契约/格式 | 一条与回归测试绑定的记录 |
| 有意更改记录或测试已固定的行为(需求/规格变更) | 在同一次更改中更新该记录及其绑定的测试;如果旧决策已废弃,使用 Superseded-by 将其归档,并将新契约记录为 spec-change——绝不要另加一条同级 bug-fix 记录 |
编写记录前,先找到已有的、负责该机制的记录(使用 --find,或查看 new-note.py 拒绝重复时列出的归属记录),然后更新它。一个机制一条记录,而不是每起事件一条。现在,如果同一测试文件已绑定记录,new-note.py 会拒绝创建同级记录;只有在拆分确实有意为之时,才传入 --allow-duplicate "<why this is a new mechanism>"。
安装到代码库
只有在该技能成为必需项,而非仅仅可用时,它才会生效。解决 $SCRIPTS(参见定位脚本)后,从代码库根目录运行以下命令即可完成安装:
python3 "$SCRIPTS/new-note.py" --install --dry-run # show what would change
python3 "$SCRIPTS/new-note.py" --install # notes tree + pre-commit hook + standing rule, then --check-install
--install 会创建记录目录树,写入一个运行校验器的 pre-commit hook(遵循 core.hooksPath),并将一条常驻规则追加到 AGENTS.md(或现有的 CLAUDE.md)中。它绝不会修改现有 hook——只会打印出需要添加的那一行;也不会重复添加已有规则。
它所写入的常驻规则、hook、CI 和打包脚本替代方案,以及让接入成本保持低廉的两条规则,都在references/gate-and-format.md中。
**只有在你能说出无需 Agent 决定是否运行校验器的命令时,才算安装了这项技能。**如果答案是“Agent 记得时就运行”,那么门禁就不存在——这正是这项技能旨在防止的问题。
定位脚本
脚本与此文件位于同一目录。不要先用 glob 搜索——只解析一次,并从成本最低的来源开始:
-
Claude Code v2.1.64+ — 加载此文件时,运行环境已将
${CLAUDE_SKILL_DIR}展开为此技能的目录。直接使用它并跳过其余步骤:SCRIPTS="${CLAUDE_SKILL_DIR}/scripts" -
其他运行环境 —
~/.agents/skills是 Agent Skills 开放标准的位置,npx skills会将其安装到大多数 Agent 使用的目录中;少数 Agent 使用自己的目录。用一条命令测试标准位置(sh/bash;在 Windows 上使用 Git Bash,或使用下面的 PowerShell 等效命令);命令会在找到第一个匹配项时退出,并且只在最后才使用 glob:for d in "${CLAUDE_SKILL_DIR:-}" "$HOME/.agents/skills" \ "$HOME/.claude/skills" "$HOME/.config/opencode/skills" \ "$HOME/.codex/skills" "$HOME/.cursor/skills" \ ".agents/skills" ".claude/skills"; do [ -f "$d/regression-notes/scripts/verify-notes.py" ] \ && SCRIPTS="$d/regression-notes/scripts" && break done if [ -z "${SCRIPTS:-}" ]; then path=$(find "$HOME" . -maxdepth 6 -path '*/regression-notes/scripts/verify-notes.py' -print -quit 2>/dev/null) [ -n "$path" ] && SCRIPTS=$(dirname "$path") fiPowerShell 等效命令:
$roots = @( $env:CLAUDE_SKILL_DIR, (Join-Path $env:USERPROFILE ".agents\skills"), (Join-Path $env:USERPROFILE ".claude\skills"), (Join-Path $env:USERPROFILE ".config\opencode\skills"), (Join-Path $env:USERPROFILE ".codex\skills"), (Join-Path (Get-Location) ".agents\skills") ) | Where-Object { -not [string]::IsNullOrEmpty($_) } $hit = $roots | ForEach-Object { Join-Path $_ "regression-notes\scripts\verify-notes.py" } | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1 if (-not $hit) { throw "Could not locate regression-notes/scripts/verify-notes.py" } $SCRIPTS = Split-Path -Parent $hit
笔记根目录默认为 .agents/notes/,可在单次调用时通过 --notes-dir 覆盖,也可通过环境变量 NOTES_DIR 全局覆盖。
从这里开始
修复 bug 时,先创建笔记,并将其关联到回归测试:
python3 "$SCRIPTS/new-note.py" --status implemented --test tests/test_login.py login-retry-race
--test 可重复使用,并会用真实测试路径预填 ## Verification。使用 --test 'tests/test_login.py::test_retry_after_lockout' 关联一个精确的测试(参见回归关联)。在测试文件存在且 Proved: 行记录真实的失败运行、替代模板占位符之前,严格验证仍会失败。
--class 接受以下封闭集合中的任意类别;bug-fix 以外的类别使用 templates/generic.md,且不要求包含 ## Verification。
一项被否决的提案:
python3 "$SCRIPTS/new-note.py" --status rejected --reason "duplicates the retry policy" login-retry-race
在编写新决策前,先找到已有相关决策的笔记:
python3 "$SCRIPTS/verify-notes.py" --notes-dir <notes-dir> --find "retry policy"
--find 会针对笔记文件名和正文匹配正则表达式,并打印每个匹配项的路径、标题、决策和引用的测试。无需维护索引文件——目录树就是索引,搜索方式如下:
python3 "$SCRIPTS/verify-notes.py" --notes-dir <notes-dir>
退出码非零表示验证失败。门禁要求恰好有一行:
python3 regression-notes/scripts/verify-notes.py --notes-dir .agents/notes
如果干运行期间笔记目录可能不存在且这属于正常情况,请使用 --allow-missing;不要在提交门禁中使用此标志。
目录结构与格式
笔记按路径编码为 {proposed,implemented,rejected,archived}/{class}/yyyy-mm-dd-topic.md:
- 生命周期是其状态:
proposed/(Status: proposed)→implemented/(Status: implemented)→archived/(Status: implemented+Archived: YYYY-MM-DD)。将proposed/移至implemented/,意味着将## Proposal改写为现在时的## Decision,并将## Acceptance criteria/## Risks合并到## Consequences中。rejected/(Status: rejected — <why>)保留提案阶段的各个章节(## Problem、## Proposal、## Alternatives considered,以及可选的## Acceptance criteria/## Risks),且仅在落选提案仍容易让人误选时保留。archived/笔记为冻结状态:一旦标记Archived: YYYY-MM-DD,就不得再编辑;每篇归档笔记都必须封存(见下文)。
- 类别是封闭集合:
bug-fix, spec-change, feature, architecture, process, testing, simplification。spec-change用于记录有意的需求变更;它使用通用模板,因此不要求Proved:失败运行,但在同一次变更中,仍必须更新用于固定旧行为的测试,并将旧笔记归档或标记为Partly-superseded-by。添加类别时,必须同时更新此文件和verify-notes.py中的CLASSES常量;new-note.py会从验证器读取类别集合,因此二者不会出现偏差。
每篇笔记都遵循同一模板。bug-fix 使用 templates/bug-fix.md;其他类别(包括 spec-change)使用 templates/generic.md,其模板与 bug-fix 相同,但不包含 ## Verification(只有 bug-fix 会关联测试):
# Agent Note: <title>
Status: implemented
## Problem
## Decision
## Alternatives considered
## Consequences
## Verification # bug-fix only
标题区块、验证器强制执行的每条规则、提示性警告,以及用于避免大型目录树产生过多输出的标志(--baseline、--changed-only、每篇笔记的 verify-disable),均见 references/gate-and-format.md。
归档与退役
一步归档已被完全取代的说明——同时移动、添加标头并封存:
python3 "$SCRIPTS/new-note.py" --archive 2026-09-01-old-decision.md --successor 2026-09-09-new-decision.md --notes-dir <notes-dir>
归档的说明会在 archived/manifest.json 中封存并冻结。当归档说明所描述的代码不复存在时,应将其退役,避免归档内容只增不减:
python3 "$SCRIPTS/new-note.py" --retire 2025-03-01-old-cache.md --reason "cache layer removed" --notes-dir <notes-dir>
封存、重新封存和退役规则见 references/gate-and-format.md。
保持语料库更新
与代码相矛盾的说明比没有说明更糟:Agent 遵循过时的权威说明,会导致修复回退。
创建说明前,先在代码树中搜索是否已有说明负责相同的决策或机制(参见上文的 --find)。在同一项更改中更新该说明,而不要重复创建。若相同的测试文件已绑定,new-note.py 会拒绝生成同级说明;对于确实独立的机制,可明确使用 --allow-duplicate "<why>" 作为例外。
后续决策取代先前决策时,应在标头中记录链接,避免旧说明被误认为当前权威说明:
- 完全取代 — 将独有的理由、替代方案、后果和验证内容整合到新的归属说明中,然后运行
new-note.py --archive <old>.md --successor <new>.md(移动到archived/、添加Superseded-by:并封存)。旧说明保留其历史记录;指针则说明当前由哪份说明负责该决策。 - 部分取代 — 后续说明涵盖一个分支,其余内容仍然有效。添加
Partly-superseded-by: <successor>.md和## Superseded章节,说明哪些部分已失效。将说明留在implemented/中:它对其余内容仍是有效的权威说明。
不要只在正文中以行内备注的形式记录部分被取代的决策——这样一来,已被取代的 ## Consequences 章节就会继续声称代码已经不再具有的行为。标头指针和 ## Superseded 章节能明确指出过时的部分。
若代码移动、重命名或删除了已实现说明中引用的文件,应在同一项更改中更新说明里的路径和符号;若说明主题已不存在,则删除说明(Git 历史会保留它)。验证器会针对缺失的测试路径、无法解析的裸文件名和失效的说明引用发出警告;它无法发现的过时模块名、默认值和正文描述,必须由你检查。
--bound-changes [--base REF] 是配套的变更期检查工具:它会列出当前差异中绑定的测试文件已更改、但说明本身未更改的已实现说明。说明应随其测试同步更新,以记录新约定;若其约定已退役,则应归档或标记 Partly-superseded-by。可在 CI 中或本地针对同一差异运行此工具;它仅提供建议,因此能找出那些不会阻碍合并的隐蔽问题。
--metrics 会让说明债务一目了然:按类别统计数量、被多份说明引用的测试文件、验证中未指定任何 tests/ 路径的 bug-fix 说明,以及取代流程的使用频率。代码树即使通过验证,也可能逐渐变成一堆针对单次事件的说明;请定期运行此工具,并将热点文件整合成一份约定说明。
回归绑定
说明与测试的链接是单向且可审查的:说明的 ## Verification 章节列出测试路径;规范本身保持为纯测试代码。两者应在同一项更改中提交——没有测试的说明只是愿望,没有说明的测试则是一桩谜团。archived/ 中的说明已冻结,不得引用为当前行为的权威依据。
要绑定一个精确的测试,请写出路径和锚点,并以 :: 分隔:
- `tests/test_login.py::test_retry_after_lockout`
锚点会作为普通子字符串在该文件中检查。语法仅此而已:验证器从不解析测试语言,因此对 test_foo(pytest)、TestFoo(Go)、should foo(JS)或快照目录名都一样有效。只有当含有 :: 的标记中,第一个 :: 之前的部分是现有文件时,它才会被视为绑定,因此 C++ 作用域解析(std::filesystem、Class::Method)绝不会被误认为绑定——无需列出语言清单。
优先使用带锚点的形式:单独写 tests/test_login.py 只能证明文件存在,不能证明测试存在。若你在 ## Verification 的任何位置按名称引用测试,却没有锚点,验证器会发出警告,因为它无法区分被重命名的测试和普通说明文字。
只有当回归问题能让防护测试失败,而且失败落在绑定所指的断言上时,防护才算有效:夹具、环境或编译错误证明不了任何事,因此要修复设置并重新运行,直到绑定的断言因预期原因失败。
两种证据都算数,并且共用一条记录:
- 自然红 — 你实际观察到的失败:修复前编写的测试(TDD),或补丁应用前测试套件的实际失败。这足以直接完成证明;无需再人为制造一次运行。
Proved:行必须引用可核验的内容,例如Proved: pre-fix run failed at <assertion>; output saved to <evidence-log>——只说“之前失败过”,却没有日志或输出,就是这行要杜绝的自述。 - 人为红 — 仅当测试是在修复后编写且从未观察到失败时才需要。保持操作简单且可逆:禁用一个防护,只运行绑定的测试(聚焦运行,例如
cargo test --lib <name>),保存失败输出,恢复防护,然后重新运行同一个聚焦测试并确保通过——恢复并重新跑绿是这个流程的一部分,不能省略。一次人为运行可以覆盖绑定到同一防护的所有测试;绑定到不同防护的测试则各自需要单独运行。
在 ## Verification 下的 Proved: 行记录红测运行(例如 Proved: disabled the latch → tests/test_login.py::test_retry_after_lockout failed as expected, then reverted; output saved to .agents/notes-evidence/login-retry-red.log)。将证据放在已提交的目录中——它应与 notes 树同级(惯例为 <notes>-evidence/),因为 notes 树本身只接受 .md 文件——不要放在 target/ 这类会被清理或被 gitignore 忽略的构建输出目录中。Proved: 行中用反引号括起的路径会被视为证据,而不是测试目标:它们永远不会导致验证失败,但如果证据文件缺失则会发出警告。因此,若 Proved: 行所引用的证明会因 cargo clean 而消失,就不会再悄无声息地通过验证。要针对真实环境验证(重新运行命令,从外部重新读取文件),绝不要针对 Agent 自己的输出验证。
笔记若无人阅读,就毫无价值。--for-path 用于反向查找:传入你即将修改的文件,即可获取引用这些文件的笔记——每条笔记列为一个条目(标题及其决策内容的首行),按时间从新到旧排列,最多返回 10 条(--limit N,0 表示全部)。归档笔记也会计入,但只有指定 --include-archived 才会列出,因为它们是历史记录,而不是当前约束。要在编辑开始时接入这一功能——添加仓库规则(“编辑前,对变更文件运行 --for-path”)、添加会打印相关笔记的 pre-commit hook,或两者兼用——因为无需任何人记得触发的提醒,比依赖记忆的仪式更有价值。对于测试会自行列出清单的仓库(cargo test -- --list、pytest --collect-only),CI 步骤可以将 --dump-anchors 的输出与真实清单取交集,以发现因重命名测试而遗留的孤立子字符串。
门禁会验证已存在的笔记;它看不到那些从未留下笔记的修复。--audit-commits [N] 从历史记录这一侧弥补了这一缺口:它会标记近期提交中,提交消息看起来是在修复问题、且改动了代码,但既未添加笔记也未添加测试文件的提交。若修复附带了回归测试,该测试就已覆盖此修复——不要为了让这份报告不再提示而额外添加笔记。这是一份报告,不是门禁——请定期运行,或在一次 Bug 修复后运行,并将标记出的提交视为可补写笔记的候选项。
何时撰写事后分析
只有当故障同时具备以下三个特点时,才撰写更深入的事后分析:隐蔽(资深人员也难以推导出原因)、系统性(测试、工具或惯例存在缺口,而非拼写错误),且重新发现代价高昂。普通 Bug 记一则笔记即可,无需撰写事后分析。
安全
验证器以只读方式打开笔记,从不执行项目代码,并将笔记内容视为不可信文本。采取行动前,请根据当前仓库验证笔记中关于测试路径的说法——路径字符串并不能证明相应行为成立。Proved: 行记录了验证流程,但作者仍可能写下虚假的内容;审查是最后一道保障。
绑定检查和过时检查只检查是否存在,并不能证明正确。锚点出现在文件中,并不意味着测试通过;而若不运行测试套件,就无法验证 Proved: 行,本技能特意不会运行测试套件。请将运行结果为绿色理解为“代码树内部一致”,而不是“修复有效”。
更改验证器
每次更改 scripts/ 后,请运行 python3 -X utf8 -m unittest discover -s regression-notes/tests;添加检查的规则见 references/gate-and-format.md。