mcpField
B
backfill

backfill

github.com/scaccogatto生产力433343 次安装1 天前更新
代码仓库

关于 backfill

backfill 是用于从 Git 提交和 Claude 会话记录重建 OKF 知识包的技能,面向需要恢复项目决策脉络的维护者。它采用确定性事件提取与可审计的 LLM 回放,并提供覆盖检查和续跑机制。

主要功能

  1. 01提取 Git 与会话事件
  2. 02按时间顺序回放事件
  3. 03并行分析并顺序归并
  4. 04支持检查点续跑
  5. 05检查事件覆盖与包结构

适用场景

  1. 01从仓库历史重建 OKF 知识包
  2. 02从中断的回放检查点继续处理
  3. 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 或打开分析文件:进入上下文的任何内容,在后续每一轮都会按前沿费率计费,而设置这种路由正是为了避免这一点。

完成:验证并清理
  1. 手动为每个目录编写 index.md(不要运行 okf_init.py——它会生成占位文件):每个目录一个 # Section,每个概念一个项目符号,使用各概念 frontmatter 中的内容(SPEC §8),格式为 * [Title](file.md) - description。根目录的 index.md 保留其 okf_version: "0.2" frontmatter,并链接到每个子目录。

  2. 添加最终日志条目(使用当天日期;如果已存在当天日期的部分,则追加到其中):

    ## 2026-09-01
    
    - **Backfill**: reconstructed from N events by okf-backfill/0.9.5
    
  3. 删除游标:

    rm -f .okf/.backfill-state.json
    
  4. 运行反退化规则自检:

    # 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)。

  5. 运行覆盖检查,确保每个事件都已映射:

    uv run "${CLAUDE_SKILL_DIR}/scripts/okf_backfill_events.py" \
      --check-coverage events.jsonl .okf
    

    退出代码 0 表示所有有效事件(未跳过)都出现在知识包的 sources 或日志中。退出代码 1 并列出未映射的 ID,表示归纳器跳过了某些事件;修复后重新运行。

  6. 验证知识包架构:

    uv run "${CLAUDE_SKILL_DIR}/../validate/scripts/okf_validate.py" .okf --strict
    

    完成前修复所有错误。

  7. 报告生成的知识包:

    • 每个目录中的概念数
    • 日志条目示例(第一条和最后一条)
    • 覆盖检查结果(所有事件均已映射)
    • 验证结果(通过/失败、警告)
    • 已取代的处理结果(归纳器的 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-generatedGit 提交仅涉及锁文件或生成代码(vendor/、node_modules/、dist/、.min.、*.lock 等)噪声:构建产物,不含知识内容
merge-no-files没有文件变更的 Git 合并提交(仅限第一父提交)例行维护
session-command-noise会话用户消息是斜杠命令,或少于 20 个字符且没有有用结果短暂的 UI 消息/闲聊

使用 --skip-globs "extra/**" 为特定仓库扩展第一条规则。

4. 防退化规则

分析器和编织器会执行以下规则,防止知识包退化成机械罗列提交或意外形成分类体系:

  1. 概念名称描述领域实体,而不是变更。 禁止使用:

    • 根据 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 (变更)。变更历史记录在 frontmatter sources 和正文的 ## History 中; log.md 仅记录生命周期事件。

  2. 优先更新,而非创建。 分析每个事件会触及哪些概念;如果某个概念已存在且与分析相符,就更新其 sources 和正文。只有分析发现尚未记录的独立领域实体时,才创建新概念。

  3. 日志项目符号记录生命周期事件,并说明意图。 只有创建、弃用或被取代的概念才添加项目符号;常规更新不添加。项目符号应根据提交正文或会话结果回答“为什么?”,而不能只是复述主题:

    • 不佳:- Added presales-pipeline.md feature
    • 良好:- **Creation**: [Presales pipeline](...), formalized to clarify handoff points
  4. 连续的项目符号不得完全相同。 同一概念连续出现多个项目符号,说明更新被错误地写进了日志:

    - Feature X update
    - Feature X update
    - Feature X update
    

    此时不应添加任何项目符号;更新应记录在概念的正文和 sources 中。

  5. 逆转不等于增长。 回放会按时间顺序遍历历史,因此后续事件经常会推翻先前的内容(删除某个阶段、修改限制、降低目标优先级)。 “优先更新,而非创建”是合并规则,与此无关:单独应用这条规则会导致知识包在现在时中、无论是在一个概念还是两个概念里,同时声称两种状态都成立。编织器会在写入前搜索整个知识包,遇到矛盾时会替换内容而非追加:正文陈述当前成立的内容,先前的立场移至 ## History 下带日期的条目;被更新概念取代的过时概念则会标记为 status: deprecated,并添加指向后继概念的链接。弃用的概念会保留其 sources,因此覆盖率仍为绿色。Finalize 中的任何检查都无法发现这一问题——这些检查是词法检查,而被取代的声明在格式上是有效的。

  6. 重建的概念应为 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 文件中的两条明确指示(标志复制哪条摘要行;声明将意图作为意图报告)。

评论

支持 Markdown