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,而模型從來看不到那裡,所以那個日期之前的結果描述的是沒人讀過的警告;它們不能把規則推向攔截。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。