Retro
指令文件靠堆积生长。每一条规则都是某人看着代理做错事之后加上去的,而没人衡量这条规则之后有没有用。Retro 把这个循环闭合。它记录护栏何时被打破,衡量每次会话花了多少 token,然后以 OpenSpec 的形式提出对指令、hook 和技能的具体修改,由你逐行批准或驳回。
什么都不会离开这台机器。什么都不会自动应用。
它记录什么
一个 Claude Code hook 解析 Claude Code 本来就保存在磁盘上的会话记录,并把元数据追加到 .coograph/signals.jsonl(被 git 忽略)。它在 SessionEnd 时运行,每次会话压缩时也运行。第二个触发点才让长会话变得可读:跑一整天的会话只会抵达一次结束,而且很晚,被强杀就根本没有;压缩是它唯一会反复触发的事件。重复采集是安全的,因为一次会话的记录是整体替换而不是追加。warn-scope.py 和 block-generated.py 这两个 hook 每次触发时也追加一条违规记录。每个规则 hook 还会追加一条决策:它做了什么(warned、blocked 或 suppressed),以及它针对的那次工具调用的 id。
每条违规都带着它发生的那一刻,取自引发它的那条消息,而不是采集运行的时刻。
每条记录包含工具名、计数、规则 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,且不存在活跃的变更目录。openspec-gate-warn.py 在同样的条件下实时警告,每次会话一次,在第二个不同的源文件被编辑时触发;检测器本身仍是启发式的 |
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 用量,以及会话记录文件大小 |
decision | 该 hook 的规则 | 确定性 | 某个规则 hook 发出了警告、拦截了操作,或抑制了一次重复警告。由 hook 写入,永远不计为违规 |
outcome | 该决策的规则 | 确定性 | 采集时为每条能在会话记录里找到对应工具调用的决策推导:调用是否照常执行,用户接下来的一句话是否是纠正,警告是否得到回应(scope 警告之后编辑了 tasks.md,门禁警告之后出现了 OpenSpec,缺陷警告之后跑了评审,被拦截的路径之后没再碰),以及同一条规则在本次会话里之后又触发了几次 |
启发式信号永远不会是一处改动的唯一证据。
决策与结果
违规次数只说明规则被破坏了,不说明警告有没有起作用。在 coograph 自己身上跑的第一次 retro 发现,scope 在一次会话里警告了 23 次,却没有办法分辨哪些警告被采纳、哪些只是噪声;no-new-deps 则凭一次真实事件加两次验证探针就读作「升级为拦截 hook」。
所以现在每个规则 hook 都记录它做了什么决策,采集则记录之后发生了什么。决策在 hook 触发的那一刻写入,带着那次工具调用的 id。会话结束时,会话记录按这个 id 和它对上,从不按时间戳,然后写出一条带四个字段的结果:proceeded(调用照常执行了)、corrected(用户接下来说的话匹配了某个纠正模式)、reconciled(之后有东西回应了这次警告;按规则定义,回应不可观察时为 null,比如依赖的批准)、repeated(同一条规则在本次会话里之后又触发了)。
这份证据决定一条规则从警告 hook 走向拦截 hook。超过阈值的 hook-warn 规则只在满足两个条件时才升级:至少有 deterministic_events 条结果,并且其中至少 escalate_ignored_rate 的比例被忽略了,也就是规则又触发了、而没有任何东西回应它。否则报告显示 hold: no_outcomes(hook 还没在决策记录下运行过)或 hold: warnings_change_behaviour(警告之后有人修正,或者再也没有重复)。被搁置的规则只观察,不改动。corrected 来自启发式模式,只展示,从不作为门槛。
结果只从一条规则的 outcomes_since 日期开始计数(可选;新项目从种子里获得它,已有项目需要手动加到 rules.json,见 MIGRATION.md),也就是 hook 行为最近一次改变的日期。2026-10-06 之前,警告 hook 打印到 stderr,而模型从来看不到 stderr,所以那个日期之前的结果描述的是没人读过的警告;它们不能把一条规则推向拦截。last_changed 仍然标记对规则的任何编辑,包括改写措辞,并且从不丢弃证据。
报告多了一张决策表:每条规则的 hook 警告、拦截、抑制了多少次,有多少条结果,以及照常执行、被纠正、得到回应和被忽略的比例。
验证探针,也就是 retro 技能为了证明某个 hook 会触发而启动的嵌套 claude -p 会话,是真实的会话,过去也被当成真实的违规来计数。rules.json 现在带有 ignore_session_prefixes,默认写入探针前缀。id 以其中任何一个开头的会话照常采集,但不进入任何计数,报告会说明排除了多少个。
OpenSpec 门禁在同一次改动里得到了第一道强制执行。openspec-gate-warn.py 在第二个不同的源文件即将被编辑、而会话中没有任何操作触碰过 openspec/changes、也不存在活跃的变更目录时,每次会话警告一次。它用的是检测器自己的分类和同一个「是否存在活跃改动」的检查,所以两者不会漂移。它只警告,不拦截,它的结果记录了警告之后有没有出现 OpenSpec。
各工具支持情况
只有 Claude Code 暴露会话记录和生命周期 hook。其他工具能拿到分析器和技能,但没有捕获。
| 工具 | 会话记录检测器 | hook 触发的信号 | 会话开始行 | 变更结束提示 |
|---|---|---|---|---|
| Claude Code / Cowork 插件 | 全部 | 违规:scope、generated-files;决策:每个规则 hook | 有 | 有 |
| 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
当指令文件超出预算时,后面还会跟着第二行,不管有没有采集到会话:[layout] N instruction file(s) over budget, see python3 .github/layout/layout.py --budget,或者在小修小补无法弥合差距时:
[layout] STRUCTURAL: apps/web/AGENTS.md over twice its budget (+5 more), run /coograph-docs-restructure
没有任何规则超过阈值时,它会这么说。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。它以一段大白话开头,然后是表格:每条规则对照阈值并附“升级到”一列(一个台阶,或者当警告 hook 没有被忽略的证据时的 hold: 原因),一张决策表,路径聚集,构建重试,每次会话的 token(以最近一次规则改动为界做前后拆分),工作流遵循度(同时跑了评审或验证技能的编辑会话),按层级列出的指令布局及结构性结论,gotcha 活动(每个条目被展示了多少次,以及它对应的命令是否仍然失败并被重试),以及归档统计。匹配 ignore_session_prefixes 的会话被整体排除,只作为「被忽略的会话」计一次数。确定性的 Python,只用标准库,不涉及模型。
提案
/coograph-retro 读取报告,写出 openspec/changes/<date>-retro-<n>/,包含提案、规格、任务和现成的补丁。改动类型:
- 改写规则:当证据显示歧义时改写已有规则
- 新增规则:没有任何规则覆盖的反复出现的模式
- 新增指令文件:一份
.github/instructions/<name>.instructions.md,用applyTo限定到违规聚集的目录 - 新增 hook:仍在被违反的文字规则变成警告 hook;警告被忽略的警告 hook 变成拦截 hook
- 删除规则:十次会话里没人碰过的非硬性文字规则
- 结构性:指令文件超出预算太多,小修小补无济于事;唯一的提案是运行
/coograph-docs-restructure - 新增 / 删除 gotcha:同一条命令上反复失败的构建变成一个
GOTCHAS.md条目;路径不匹配任何文件的条目被删除 - 升级 gotcha:一个 gotcha 不断被展示,而它针对的命令仍然失败并被重试,就会得到一份文字提案,建议用一条 lint 规则或 hook 让这个陷阱不可能再发生
每一处改动都以三句大白话开头:发生了什么、为什么重要、改什么;后面是一个证据块,数字只来自报告。你批准后,用 /coograph-apply 像其他变更一样应用。
循环的规则
- 仍在被违反的文字规则会被升级为 hook。永远不会被改写得更大声;数据说那没用。
- 警告 hook 升级为拦截 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 的项目引导启动所需的归档变更数 |
escalate_ignored_rate | 0.5 | 一个警告 hook 的结果里被忽略的比例达到多少、且至少有 deterministic_events 条结果时,Retro 才会提议拦截 hook |
它不是什么
Retro 不是模型意义上的递归自我改进。没有权重会改变,没有模型自己训练自己。它是指令层的版本:系统观察自身的失败并对自身的脚手架提出修改,由人守在闸门前。规则能否在多轮之后持续改进还没有被证明;第一次 retro 通常找到大部分价值。完整的检测器参考、注册表格式和测试在仓库的 .github/retro/README.md。