Retro
指令文件靠堆积生长。每一条规则都是某人看着代理做错事之后加上去的,而没人衡量这条规则之后有没有用。Retro 把这个循环闭合。它记录护栏何时被打破,衡量每次会话花了多少 token,然后以 OpenSpec 的形式提出对指令、hook 和技能的具体修改,由你逐行批准或驳回。
什么都不会离开这台机器。什么都不会自动应用。
它记录什么
一个 Claude Code hook 解析 Claude Code 本来就保存在磁盘上的会话记录,并把元数据追加到 .coograph/signals.jsonl(被 git 忽略)。它在 SessionEnd 时运行,每次会话压缩时也运行。第二个触发点才让长会话变得可读:跑一整天的会话只会抵达一次结束,而且很晚,被强杀就根本没有;压缩是它唯一会反复触发的事件。重复采集是安全的,因为一次会话的记录是整体替换而不是追加。warn-scope.py 和 block-generated.py 这两个 hook 每次触发时也追加一条记录。
每条违规都带着它发生的那一刻,取自引发它的那条消息,而不是采集运行的时刻。
每条记录包含工具名、计数、规则 id、仓库相对文件路径、shell 命令的哈希和程序名、时间戳和 token 用量。它永远不包含提示文本、助手文本、工具输出、文件内容或完整命令。允许列表写在代码里强制执行,还有一个哨兵测试:在一份会话记录的每个部分种下标记字符串,只要标记进了信号文件就失败。
检测器
| 检测器 | 规则 | 置信度 | 触发条件 |
|---|---|---|---|
graph-first | graph-first | 确定性 | 在本次会话第一次访问代码图之前(mcp__code-graph__* 工具,或同时包含 sqlite3 和 .code-graph 的 shell 命令)出现了 Grep 或 Glob 调用,且 .code-graph/graph.db 存在。会记录之后是否碰到了代码图(mcp-later、sqlite-later)还是从未碰到(total-bypass)。子代理的调用不计。 |
openspec-gate | openspec-gate | 启发式 | 编辑了两个以上源文件,会话中没有任何操作触碰 openspec/changes,且不存在活跃的变更目录 |
scope-warning | scope | 确定性 | warn-scope.py 打印了它的警告 |
generated-file-block | generated-files | 确定性 | block-generated.py 拦截了一次编辑 |
build-retry | 无 | 确定性 | 同一条 shell 命令跑了三次以上,其中至少两次失败 |
user-correction | user-correction | 启发式 | 一条用户消息在工具调用之后匹配了某个纠正模式(只记录模式 id)。围绕同一主题反复纠正,是「某条指令缺失或含糊」最清楚的证据,所以它们归到自己的规则下聚类 |
defect | defect | 确定性 | 会话时间窗内的一次修复提交,落在了过去 14 天内被非修复提交改过的文件上。每对「修复提交 + 来源提交」出一条信号,只带文件列表和两个短哈希,别的什么都不带。项目根是仓库就扫根目录,否则扫它下面一层的各个仓库,因此根目录下放着 app/ 和 admin/ 的项目也覆盖得到 |
new-dependency | no-new-deps | 确定性 | npm install <pkg>、pip install <pkg>、uv add、cargo add、go get 之类,或对依赖清单的编辑。安装清单里已有的东西不算:pip install -r requirements.txt、pip install -e .、不带参数的 npm install |
session | 无 | 确定性 | 总是记录:消息数、用过的工具、编辑的文件数、调用的技能、代码图是否存在、开始与结束时间、按会话汇总的 token 用量,以及会话记录文件大小 |
启发式信号永远不会是一处改动的唯一证据。
各工具支持情况
只有 Claude Code 暴露会话记录和生命周期 hook。其他工具能拿到分析器和技能,但没有捕获。
| 工具 | 会话记录检测器 | hook 触发的信号 | 会话开始行 | 变更结束提示 |
|---|---|---|---|---|
| Claude Code / Cowork 插件 | 全部 | scope、generated-files | 有 | 有 |
| Codex CLI | 无 | 无(仅 Bash 审计日志) | 无 | 有 |
| OpenCode | 无 | 无(仅 Bash 审计日志) | 无 | 有 |
| Cursor、Devin Desktop、Aider、Cline | 无 | 无 | 无 | 有 |
你怎么知道
你不需要记住任何步骤。每一次 Claude Code 会话开始时都会打印一行,位置就在代码图状态已经出现的地方:
[retro] 12 sessions captured, 3 rules over threshold (graph-first 11x), run /coograph-retro
没有任何规则超过阈值时,它会这么说。Retro 没启用、但项目已经归档了十个以上 OpenSpec 变更时,它会说 run /coograph-retro to bootstrap。
coograph-archive 和 coograph-apply 也会在一次变更结束时问一次:Run /coograph-retro now? (n sessions since last retro)。默认是否。代理永远不会未经提示就跑 retro。
第一天,不是第二十天
Claude Code 把每一份历史会话记录保存在 ~/.claude/projects/<slug>/ 下,其中 <slug> 是项目的绝对路径,把 A-Z a-z 0-9 之外的每个字符替换成 -(例如 C--paul-code-app 或 -home-paul-app)。初始化时会问你要不要读取它们,所以第一份报告里立刻就有真实的会话:
python3 .claude/hooks/capture-signals.py --backfill ~/.claude/projects/<slug> --cwd .
从没启用过 Retro、但已经有至少十个归档变更的项目,可以直接运行 /coograph-retro 引导启动。技能会初始化注册表,询问会话记录目录,回填,然后像正常运行一样继续。
报告
python3 .github/retro/retro.py --report
写出 .coograph/retro/report.md。它以一段大白话开头,然后是表格:每条规则对照阈值并附“升级到”一列,路径聚集,构建重试,每次会话的 token(以最近一次规则改动为界做前后拆分),工作流遵循度(同时跑了评审或验证技能的编辑会话),指令文件相对 token 预算的大小,以及归档统计。确定性的 Python,只用标准库,不涉及模型。
提案
/coograph-retro 读取报告,写出 openspec/changes/<date>-retro-<n>/,包含提案、规格、任务和现成的补丁。五种改动类型:
- 改写规则:当证据显示歧义时改写已有规则
- 新增规则:没有任何规则覆盖的反复出现的模式
- 新增指令文件:一份
.github/instructions/<name>.instructions.md,用applyTo限定到违规聚集的目录 - 新增 hook:仍在被违反的文字规则变成警告 hook;仍在被违反的警告 hook 变成拦截 hook
- 删除规则:十次会话里没人碰过的非硬性文字规则
每一处改动都以三句大白话开头:发生了什么、为什么重要、改什么;后面是一个证据块,数字只来自报告。你批准后,用 /coograph-apply 像其他变更一样应用。
循环的规则
- 仍在被违反的文字规则会被升级为 hook。永远不会被改写得更大声;数据说那没用。
- 启发式信号永远不是唯一证据。
- 超出指令 token 预算后,每新增一条规则必须配对删掉一条。
- Retro 可以对它自己的技能、检测器和 hook 提出修改,但不能改阈值或禁用检测器,除非提案里有明确的
Loosen:任务。 - 每个生成的 hook 都带来源头注释。每份 retro 提案都以回滚任务结尾。
Episode
阈值计的是 episode,不是会话。一个 episode 就是「某个会话在某个自然日」。
单靠会话 id 撑不起阈值。习惯开长会话的人,好几天才产生一个 id,于是「三次会话」这种门槛无论规则被破坏多少次都够不着;而频繁重启的人,靠噪声就能越过它。按「会话 × 日」计数让两种工作方式可比,也意味着在一个长会话里、分三天各破坏一次的规则真的会达到阈值——而这正是过去按会话计数会掩盖掉的情形。报告里两个数字都给出来,所以三次短会话看起来依然和一次长会话不一样。
阈值
全部在 .github/retro/rules.json 里,可按项目调整。名字里带 session 的那几项,计的是 episode。
| 阈值 | 默认值 | 含义 |
|---|---|---|
deterministic_events / deterministic_sessions | 3 / 2 | 确定性规则在这么多个 episode 里出现这么多次事件即超过阈值 |
heuristic_events / heuristic_sessions | 5 / 3 | 启发式规则成为仅供佐证的证据 |
prune_sessions | 10 | 非硬性文字规则在这么多个 episode 里零事件即为删除候选 |
defect_lookback_days | 14 | 缺陷探测器往回找「修复所落在的那次改动」的天数 |
instruction_token_budget | 8000 | 超过后,每新增一条规则必须配对一次删除 |
retro_prompt_min_sessions | 3 | 上次 retro 之后累积多少个 episode,工作流才会提议再做一次 |
bootstrap_min_archives | 10 | 从没启用过 Retro 的项目引导启动所需的归档变更数 |
它不是什么
Retro 不是模型意义上的递归自我改进。没有权重会改变,没有模型自己训练自己。它是指令层的版本:系统观察自身的失败并对自身的脚手架提出修改,由人守在闸门前。规则能否在多轮之后持续改进还没有被证明;第一次 retro 通常找到大部分价值。完整的检测器参考、注册表格式和测试在仓库的 .github/retro/README.md。