关于 backfill
backfill 是用于从 Git 提交和 Claude 会话记录重建 OKF 知识包的技能,面向需要恢复项目决策脉络的维护者。它采用确定性事件提取与可审计的 LLM 回放,并提供覆盖检查和续跑机制。
主要功能
- 01提取 Git 与会话事件
- 02按时间顺序回放事件
- 03并行分析并顺序归并
- 04支持检查点续跑
- 05检查事件覆盖与包结构
适用场景
- 01从仓库历史重建 OKF 知识包
- 02从中断的回放检查点继续处理
- 03验证事件是否都纳入知识包
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ scaccogatto/ okf-skills --skill backfill安装后在 Claude Code 中输入/backfill确认已加载。
附带文件scripts/okf_backfill_events.py
详细介绍
根据历史重建 OKF 知识包
此技能会重放仓库的决策历史(git 提交和 Claude 会话记录),重建其 OKF 知识包,效果如同 okf 技能的 Stop 钩子从一开始就处于启用状态。
提取层是确定性的(相同仓库 → 字节级一致的事件);重放层则是一个 LLM 循环,可重放、可审计,但并非字节级一致(时间戳和摘要会因运行而异)。请参阅事件架构(§1)和跳过规则(§3)。
1. 事件架构
Git 提交和会话轮次会转换为事件(JSONL,每行一个,按时间戳再按来源排序):
{"id":"git:<sha>","source":"git","ts":"<ISO8601 Z>","sha":"...","author":"...","subject":"...","body":"...","files":[{"path":"...","add":N,"del":N}]}
{"id":"session:<file>:<lineno>","source":"session","ts":"<ISO8601 Z>","user":"...","outcome":"...","title":"...","branch":"...","skip":"..."}
- Git 事件来自
git log --first-parent --numstat。 - 会话事件将每条用户消息与该轮最后一个助手文本块(收尾内容)配对,从
~/.claude/projects/<repo-slug>/*.jsonl及工作树子目录中提取。 - 时间戳会规范化为以
Z结尾的 UTC ISO 8601 字符串。 - skip 字段(可选,由提取过程添加):标记低信号事件,参见 §3;重放过程绝不会覆盖此字段。
2. 协议:预检 → 提取 → 初始化 → 重放(Map+Reduce)→ 收尾
预检:检查是否可以重建知识包
if [ -d <repo>/.okf ] && ! [ -f <repo>/.okf/.backfill-state.json ]; then
echo "ERROR: .okf/ exists but is incomplete. Delete it to rebuild from scratch, or pass --resume to continue from the last checkpoint."
exit 1
fi
如果 .backfill-state.json 存在,则从游标处继续;否则,执行全新初始化。
提取:生成事件流
uv run "${CLAUDE_SKILL_DIR}/scripts/okf_backfill_events.py" <repo-dir> \
--out events.jsonl \
[--no-sessions] [--sessions-dir ~/.claude/projects] \
[--max-text 2000] [--skip-globs "vendor/**"]
将 events.jsonl 写入暂存区(绝不提交)。报告:
- 提取的事件总数、按来源统计的数量,以及按规则统计的跳过数量。
初始化:初始化知识包(仅全新初始化时)
创建 .okf/,其中包含:
mkdir -p <repo>/.okf
echo 'okf_version: "0.2"' > <repo>/.okf/index.md
echo '# Update Log' > <repo>/.okf/log.md
游标(.okf/.backfill-state.json):
{"last_id": null, "done": 0}
重放:两阶段 Map/Reduce 协议
重放现在分为两个阶段,以确保语义深度并遵守反退化规则:
阶段 1:Map(并行分析,逐事件处理)
通过 Claude Code 的 Workflow agentType 参数,分批启动 okf:event-analyzer Agent 分析实时事件(即没有 skip 字段的事件),每批 4 到 10 个(参见 §5 中的成本说明)。每个分析器会接收其事件 ID、events.jsonl 路径、仓库路径、输出目录和发射器路径,并且:
- 使用
jq获取自己的事件 JSON(编排器只分发 ID,从不分发内容) - 仅通过受限差异发射器(§6)读取 Git 证据;会话事件无需其他调用
- 将
analyses/<event-id>.md(事件 ID 中的:替换为-)写入暂存区,并复制发射器最后一行中的truncated标记 - 为每个事件回复一行计数;分析内容绝不会通过回复返回
- 绝不触碰
.okf/ - 支持续跑:跳过
analyses/中已有的事件
分发规则(确定性,基于 events.jsonl,不读取内容):如果实时事件是会话轮次,或是 numstat 合计不超过 60 行变更的提交,则属于小型事件。小型事件按时间顺序分组,每次分析器调用处理八个;大型提交则每次调用处理一个。在此仓库上的测量结果(benchmark/map-tier/RESULTS.md):调用次数从 147 次降至 57 次,在相同档位下,Map 阶段耗时减少三分之一,所有单事件指标均未下降,也没有跨事件污染。
jq -c 'select(.skip==null) | {id, small: (.source=="session" or (([.files[]?|.add+.del]|add)//0) <= 60)}' events.jsonl
与宿主无关的备用方案(不支持 Workflow 时):使用与 agents/event-analyzer.md 相同的系统提示词启动通用子 Agent,每个事件一个,并将分析结果收集到暂存区。
第 2 阶段:归纳(顺序折叠,由一个 Agent 执行)
运行一个 okf:bundle-weaver Agent,它将:
- 按时间顺序读取所有分析
- 将其归纳到
.okf/中,并更新或创建概念 - 强制执行反退化规则(§4)
- 仅在发生生命周期事件时(创建、弃用或取代某个概念),添加一条带日期的
log.md条目;常规更新的“原因”写入概念正文 - 管理游标(
.okf/.backfill-state.json),以支持恢复 - 作为唯一可以写入
.okf/的执行者 - 用一行报告计数(已折叠、已创建、已更新、条目数、冲突、已取代、被截断的输入);绝不在回复中返回知识包
**恢复行为:**两个阶段都支持恢复。阶段 1 跳过已分析的事件 ID;阶段 2 从游标中的 last_id 重新开始。
领域预热(建议性)
开始绘制知识地图之前,阅读代码仓库的 README 和目录结构,勾勒候选概念分类(技能、集成、决策等)。这种预热仅供参考——它有助于分析器生成更好的候选名称。确保没有事件丢失的保障来自 Finalize(§5)中的确定性覆盖检查,而非预热;未包含候选名称的分析仍会交由归纳器处理。
上下文管理(协调器)
协调器只处理 ID 和计数,不处理内容。事件、分析和差异由 Agent 读取;协调器读取:
wc -l events.jsonl # total events
jq -r 'select(.skip==null) | .id' events.jsonl # live ids to dispatch
ls analyses | wc -l # analyses written
绝不要在协调器中运行 cat events.jsonl 或打开分析文件:进入上下文的任何内容,在后续每一轮都会按前沿费率计费,而设置这种路由正是为了避免这一点。
完成:验证并清理
-
手动为每个目录编写
index.md(不要运行okf_init.py——它会生成占位文件):每个目录一个# Section,每个概念一个项目符号,使用各概念 frontmatter 中的内容(SPEC §8),格式为* [Title](file.md) - description。根目录的index.md保留其okf_version: "0.2"frontmatter,并链接到每个子目录。 -
添加最终日志条目(使用当天日期;如果已存在当天日期的部分,则追加到其中):
## 2026-09-01 - **Backfill**: reconstructed from N events by okf-backfill/0.9.5 -
删除游标:
rm -f .okf/.backfill-state.json -
运行反退化规则自检:
# No concept filenames derived from change type ! find .okf -name "*.md" | grep -E "merge-pull-request|(^|/)(feat|fix|chore|docs)[:-]" # No non-kebab-case characters in filenames ! find .okf -name "*.md" | grep -E "[^a-z0-9/.-]" # No identical consecutive log bullets awk 'p==$0 && /^- / {exit 1} {p=$0}' .okf/log.md # Every concept declares its lifecycle (absent status reads as `stable`, SPEC §5.4) [ -z "$(grep -rL '^status:' --include='*.md' .okf | grep -v '/index.md$\|/log.md$')" ]这些是词法检查,不会发现知识包断言了已被取代的状态。该检查属于语义检查,应由归纳器在折叠时执行,此时它同时拥有分析结果和磁盘上的早期概念(§4 规则 5)。
-
运行覆盖检查,确保每个事件都已映射:
uv run "${CLAUDE_SKILL_DIR}/scripts/okf_backfill_events.py" \ --check-coverage events.jsonl .okf退出代码 0 表示所有有效事件(未跳过)都出现在知识包的
sources或日志中。退出代码 1 并列出未映射的 ID,表示归纳器跳过了某些事件;修复后重新运行。 -
验证知识包架构:
uv run "${CLAUDE_SKILL_DIR}/../validate/scripts/okf_validate.py" .okf --strict完成前修复所有错误。
-
报告生成的知识包:
- 每个目录中的概念数
- 日志条目示例(第一条和最后一条)
- 覆盖检查结果(所有事件均已映射)
- 验证结果(通过/失败、警告)
- 已取代的处理结果(归纳器的
superseded计数)和已弃用概念:grep -rl '^status: deprecated' --include='*.md' .okf | wc -l - 每个阶段启动的 Agent 数(分析器、归纳器调用次数);主机提供相关数据时,也报告每个阶段的 token 数
- 被截断的分析数:
grep -l '^truncated: true' analyses/*.md | wc -l,并列出 git 事件中完整差异超过默认上限的确定性估算值:jq -r 'select(.skip==null and .source=="git") | [.files[]|.add+.del] | add' events.jsonl | awk '$1>300' | wc -l
3. 跳过规则
与下列规则匹配的事件会标记为 skip: <rule-id>,并在回放期间跳过(游标前进,不写入日志条目):
| 规则 | 条件 | 原因 |
|---|---|---|
paths-only-generated | Git 提交仅涉及锁文件或生成代码(vendor/、node_modules/、dist/、.min.、*.lock 等) | 噪声:构建产物,不含知识内容 |
merge-no-files | 没有文件变更的 Git 合并提交(仅限第一父提交) | 例行维护 |
session-command-noise | 会话用户消息是斜杠命令,或少于 20 个字符且没有有用结果 | 短暂的 UI 消息/闲聊 |
使用 --skip-globs "extra/**" 为特定仓库扩展第一条规则。
4. 防退化规则
分析器和编织器会执行以下规则,防止知识包退化成机械罗列提交或意外形成分类体系:
-
概念名称描述领域实体,而不是变更。 禁止使用:
- 根据 git 提交主题生成的文件名:
merge-pull-request-#2-....md、feat:-add-docs-skill.md、fix:-handle-edge-case.md - 单独的动作词:
update.md、fix.md、add-feature.md - 非 kebab-case 格式:空格、下划线、大写字母、特殊字符(仅允许
[a-z0-9-])
示例:主题为“feat: add presales pipeline”的提交触及了一个领域概念。 概念应命名为
presales-pipeline.md(实体),而不是feat:-add-presales-pipeline.md(变更)。变更历史记录在 frontmattersources和正文的## History中;log.md仅记录生命周期事件。 - 根据 git 提交主题生成的文件名:
-
优先更新,而非创建。 分析每个事件会触及哪些概念;如果某个概念已存在且与分析相符,就更新其
sources和正文。只有分析发现尚未记录的独立领域实体时,才创建新概念。 -
日志项目符号记录生命周期事件,并说明意图。 只有创建、弃用或被取代的概念才添加项目符号;常规更新不添加。项目符号应根据提交正文或会话结果回答“为什么?”,而不能只是复述主题:
- 不佳:
- Added presales-pipeline.md feature - 良好:
- **Creation**: [Presales pipeline](...), formalized to clarify handoff points
- 不佳:
-
连续的项目符号不得完全相同。 同一概念连续出现多个项目符号,说明更新被错误地写进了日志:
- Feature X update - Feature X update - Feature X update此时不应添加任何项目符号;更新应记录在概念的正文和
sources中。 -
逆转不等于增长。 回放会按时间顺序遍历历史,因此后续事件经常会推翻先前的内容(删除某个阶段、修改限制、降低目标优先级)。 “优先更新,而非创建”是合并规则,与此无关:单独应用这条规则会导致知识包在现在时中、无论是在一个概念还是两个概念里,同时声称两种状态都成立。编织器会在写入前搜索整个知识包,遇到矛盾时会替换内容而非追加:正文陈述当前成立的内容,先前的立场移至
## History下带日期的条目;被更新概念取代的过时概念则会标记为status: deprecated,并添加指向后继概念的链接。弃用的概念会保留其sources,因此覆盖率仍为绿色。Finalize 中的任何检查都无法发现这一问题——这些检查是词法检查,而被取代的声明在格式上是有效的。 -
重建的概念应为
draft,绝不能是stable。 SPEC §5.4 将缺失的status视为stable(“可供使用”);从未经过重新阅读的知识包应为draft(“尚未审核;可能不完整”)。编织器会为每个概念写入status:。
5. 实现说明
- 确定性:提取结果逐字节一致;重放结果则不一致(时间、大模型差异)。 两者均可审计:events.jsonl 具有确定性,映射分析会被存储且可恢复,归并循环易于人工阅读并由游标跟踪。
- 隐私:events.jsonl 写入暂存区,绝不提交。提取前会截断会话轮次文本(保留开头和结尾)。
- 成本说明:深度重放会针对每个 git 提交读取一个有上限的差异(§6),以提取决策依据。对于超过 500 个事件的历史记录,可考虑拆分为多个子范围并按顺序重放,或使用
--skip-globs排除低信号路径(例如,供应商依赖项、生成的代码)。映射阶段以 4 到 10 个为一波并行执行:门槛基准测试有 299 次运行中的 260 次因高并发集体失败而未通过,最终以并发数 4 完成(benchmark/gate/RESULTS.md)。归并是顺序执行,但成本低得多(概念已完成分析)。 - 未来的数据源(尚未实现):GitHub PR/议题文本、发行说明、CI/部署日志——均为 MVP 后的可选项。Lore 协议(arXiv 2603.15566):对于采用 git trailers(结构化决策元数据)的仓库,提取事件的
body已包含机器可读格式的 trailers。分析器和编织器应将 trailers 作为“原因”的主要来源(优先于根据 diff 内容进行的通用推断)。 - 信任元数据:
generated.by是回填 Agent(okf-backfill/0.9.5),不声称经过人工审核(human:...);概念正确标记为unverified(SPEC §5.3)和status: draft(§5.4)——这两个维度彼此独立:信任表示是谁提出的,生命周期表示是否重新阅读过。这里写入status并非可选:若缺失,§5.4 会将其视为stable,因此,一个无人重新阅读过的包会自称已可供使用。 映射阶段的分析是工作产物(在归并期间保存以便审计);编织器的输出才是规范包。
6. 有上限的 diff 生成器
分析器绝不读取原始的 git show:执行框架会盲目截断过长的工具输出(在 hunk 中间截断、按字符截断,且结果取决于主机),而成本较低的 worker 可能不会注意到截断。提取器则会读取完整的 diff,并生成确定性的样本:
uv run "${CLAUDE_SKILL_DIR}/scripts/okf_backfill_events.py" <repo-dir> --show <sha> \
[--only <path>] [--max-diff-lines 300] [--per-file-lines 120] [--max-line-chars 400] \
[--skip-globs "vendor/**"]
- 与第一个父提交进行 diff,以确保合并提交与提取的 numstat 一致。
- 统计信息始终完整:无论上限是多少,实体都会保留。
- 按文件限制补丁大小(优先覆盖广度,而非深度),并在 hunk 或文件边界处截断;每处截断都会添加方括号标记;超过
--max-line-chars的行会缩短并添加标记(生成的单行文件也算“1 行”,但可能有数百 KB)。 - 即使在混合提交中,也会省略生成文件(lockfiles、
vendor/**、--skip-globs)的补丁;其统计行仍会保留。 - 最后一行采用固定格式:
[diff: shown=X total=Y files_shown=A files_total=B truncated=true|false]。 分析器会将truncated复制到其 frontmatter 中;finalize 会报告数量。 - 当截断的 diff 隐去了决策依据时,唯一允许的后续操作是
--only <path>: 沿用相同的上限逻辑,仅处理一个文件。
默认值可确保单次调用在执行框架限制内留有余量;可通过标志按仓库调整。
分析器的层级属于配置,而非基础设施:agents/event-analyzer.md 的 frontmatter 中包含 model 和 effort。分叉该文件即可调整映射阶段的层级;该技能会按名称解析 Agent,其他内容无需更改。默认值为 haiku,依据是映射层级基准测试(benchmark/map-tier/RESULTS.md):对于提交,在映射成本不到一半的情况下,与 sonnet 的结果一致;前提是保留 Agent 文件中的两条明确指示(标志复制哪条摘要行;声明将意图作为意图报告)。