让 Agent 自己走完整个流水线

mattpocock/course-video-manager · AI 原生开发实践逆向复盘

这不是一份「用 AI 写代码」的心得,而是一份把软件开发流程改造成机器可执行状态机的工程档案。 15 个月、3534 次提交、483 个 PR,作者把「领域词汇、工程纪律、架构决策、验收标准」全部写成了 Agent 能直接读懂并自我约束的文本, 再用 GitHub label 当状态机驱动一支无人值守的智能体流水线。

分析对象 mattpocock/course-video-manager 提交跨度 2025-07-23 → 2026-10-30 提交 3534 AI 参与 75.6% 取证方式 仓库契约 + git 历史 + 代码

01一页看懂

3534
次提交 / 15.4 个月
75.6%
提交带 Claude 署名
(排除合并提交口径)
483
合并的 PR
347k
行 TypeScript 代码
4809
行「AI 可读契约」文本
56
份架构决策记录(ADR)
19
个静态守卫脚本
126k
行测试(653 个文件)
AFK 流水线:label 是唯一的状态,人是唯一的合并者 PRD 议题 人 / 定时评审产出 ① To Issues agent:to-issues ③ Implement PRD 逐个切片的循环 ④ Review agent:review 人合并 永不自动 独立议题 agent:implement ② Implement 红-绿-重构 Draft PR 编排器创建 ⑤ Implement PR 回应人的评论 人评论 人解决线程 ⑦ 队列提升 阻塞者关闭时 ⑥ Update Branch 合并主干解冲突 verify-cvm 真实浏览器自证 ⑧ Architecture Review 每工作日定时 · 只读扫描 · 产出 PRD 智能体与编排器的唯一分界 编排器 · GitHub Actions 持有所有 token:打标签、推分支 建 PR、发评论、开关议题 做并发控制、失败分类、重试 是唯一有权改远端的东西 智能体运行器 · Sandcastle / Claude Code 只在本地工作树里做事 输出 = 一个目录里的若干文件 不持有 GitHub token 换掉它,整条流水线不用改
八个 GitHub Actions 工作流由 label 触发,构成一条闭环流水线。唯一没有箭头指向「合并」的一步是智能体自己完成的——人总是最后一道闸。

三个反直觉的事实

  1. AI 参与率被统计口径骗了 20 个百分点。直接数「带 Claude 署名的提交 / 总提交」只有 56.5%,但仓库里有 940 个合并提交(并行 worktree 太多所致),它们从不带署名。剔除后真实比例是 75.6%。凡是用 git 统计 AI 参与度,先问一句「合并提交算不算分母」。
  2. AI 参与率在 2026 年 8—9 月跌到 36%,不是失败,是决策密度太高。那两个月作者一个人在写 ADR 0024—0031(8 份架构决策)并攻坚 Animatic 视觉分镜。决策没定型时,Agent 帮不上忙——它只能加速「已决定」的部分。
  3. 766 个提交带 RALPH: 前缀,但 2026 年 6 月之后归零。那个「无人值守批处理循环」被更可控的 label 状态机取代了。作者自己淘汰了自己的第一代自动化。

02这个项目在做什么

Course Video Manager(CVM)是 Matt Pocock(TypeScript 教育者,Total TypeScript 作者)为自己的课程生意写的单人内部工具:管理课程结构、分镜脚本、录制、剪辑、导出、发布到 YouTube / TikTok,一路到交付日历。它不是一个玩具项目——

体量

  • 347,284 行 TypeScript(2,128 个 .ts/.tsx 文件)
  • 126,219 行测试(653 个测试文件)
  • 3 个 workspace 包 + 2 个 app + 1 个独立 Remotion 渲染器
  • 56 份 ADR,15,805 行 Markdown 文档

特殊性:它连着真实世界

  • ffmpeg 编解码、OBS 录制、Stream Deck 硬件
  • Dropbox / YouTube / Buffer / AI Hero 外部服务
  • Windows 侧进程、GPU 上的本地 Whisper
  • 作者的生产数据库就在他本机上跑

最后一条是整个实践的分水岭。这不是一个「Agent 搞砸了就 git reset」的项目——写错一行,可能毁掉的是正在录制的素材或生产数据。后面你会看到,作者对「隔离」的偏执程度,直接来自这个前提。

0315 个月的转型曲线:从手工到无人值守

把每个月的提交数(柱)和 AI 参与率(折线)叠在一张图上,能清楚看到这条曲线的形状。注意纵轴已经剔除合并提交,否则 2026-10 的 1354 次提交里有一大半是 merge,会把参与率压到 40% 的假象上。

按月:提交量(柱)与 AI 参与率(折线) 口径:排除 940 个合并提交;参与率 = 带 Co-Authored-By: Claude 署名的提交占比 87% 58% 29% 0 07 08 09 10 11 12 01 02 03 04 05 06 07 08 09 10 2026 年起 ■ 月度提交量(最高 582) ━ AI 参与率 ● 谷底 36% — 决策密集期 8—9 月谷底:密集决策 + 视觉攻坚
数据来源:git log --no-merges --grep='Co-Authored-By: Claude' 按月聚合。2025 年下半年是纯手工期,2026 年 1 月突然跃升到 87%,此后稳定在 70—91%。

四个阶段

阶段时间AI 参与在发生什么
手工期2025-07 → 120%188 次提交,全部人写。脚手架(create-react-router)、Drizzle、db 层、课程视图。唯一的例外是 10 月的 4 次尝试。
跃迁期2026-01 → 0387→91%提交量从 326 冲到 582。RALPH: 前缀提交暴增(2 月 153、3 月 474)——无人值守批处理循环在跑。
平台化期2026-04 → 0770→78%AFK 平台成型:8 个 GitHub Actions 工作流、label 状态机、Sandcastle 运行器契约。RALPH 循环退场(6 月 0、7 月 18)。
质量期2026-08 → 1036→89%8—9 月谷底是 ADR 决策 + Animatic 攻坚;10 月回到 89%,同时 .claude/skills/verify-cvm/ 长到 417 行——重点从「让 AI 写代码」转向「让 AI 自证正确」。

04底层:四层上下文契约

这个项目最贵的资产不是代码,是那 4809 行写给 Agent 读的文本。它们按「加载时机」分成四层,Agent 在不同阶段读不同的层:

上下文分层:谁在什么时候读什么 L1 · 入口导航 81 行 · 每次会话都读 · 只做指路,不复制内容(避免多份漂移) CLAUDE.md L2 · 领域词汇 578 行 · 动手前读 · 每个概念带「避免使用」反义词表 · 是 Agent 输出的词表 GLOSSARY.md L3 · 已锁定的决策 56 份 · 只读相关领域 · 决定「为什么是这样」 · 冲突必须显式声明,不许静默覆盖 docs/adr/ L4 · 工程纪律 409 行 · 写代码时读,不只评审时读 · 再由 19 个脚本编译成 exit code CODING_STANDARDS.md
四层不重复、不互相复制。CLAUDE.md 明确只做引用与指路,把细节下沉到 docs/——这样纪律文本只有一份,不会漂移。

作者对这个分层的态度写在 CLAUDE.md 里,值得原样引用:

“Read it while writing code, not only while reviewing it: "every any is a leak" binds the hand that writes the cast. It sits at the repo root rather than in .sandcastle/ for that reason.”

在写代码的时候读它,而不是只在评审的时候读。「每个 any 都是一处泄漏」这句话约束的是写下那句 cast 的那只手。所以它放在仓库根目录,而不是藏在自动化目录里。

05实践一:词汇即接口

多数团队给 Agent 的上下文是「代码库 + 任务描述」。这个项目的第一层给的是一张命名表——578 行的 GLOSSARY.md,每个词条长这样:

**Course**:
The primary domain entity: a structured collection of versions, sections,
lessons, and videos. It is not backed by any repository on disk (ADR 0018).
_Avoid_: Repo, Project

**Draft Version**:
The one CourseVersion that can still change, and the only one that accepts
writes to sections, lessons, videos and clips. There is exactly one per course.
_Avoid_: Current version, Working version

注意三件事:

docs/agents/domain.md 把这条纪律变成了对 Agent 的硬要求,并给出了唯一的正确反应:

“If your output contradicts an existing ADR, surface it explicitly rather than silently overriding:
_Contradicts ADR-0007 (event-sourced orders) — but worth reopening because…_”

如果你的输出和已有 ADR 冲突,显式说出来,不要静默覆盖。

可迁移要点

术语表的作用不是「统一叫法」,是给 Agent 一个不允许自由发挥的命名空间。成本极低(写一个词条 5 分钟),收益极高(避免整类概念性 bug)。如果你的 Agent 老是在造同义词,缺的就是这张表。

06实践二:决策沉淀为 ADR,且必须写下被否方案

56 份 ADR 覆盖从「课程版本快照」到「录制服务何时启动」的全部结构性决定。但真正有价值的是它们的写法——每份都有 Rejected alternatives 和 Consequences 两节。看 ADR 0008 的片段:

## Why
A PRD's sub-issues are not independent — earlier sub-issues frequently
subsume later ones. By the time the agent reaches sub-issue 4, the rename /
refactor / cascade work it describes may already have landed in the commit
that closed sub-issue 2.

## Rejected alternatives
- **Check "branch is ahead of `main`" instead of "this run produced commits".**
  Closer to the right invariant, but still wrong: it would fail a legitimate
  first-sub-issue scenario...
- **Reorder the workflow so PR creation precedes sub-issue close**...
  Not adopted yet — current ordering is fine once the no-commit case is
  accepted as success.

这份 ADR 记录的是一次系统性的自我纠错:原来的守卫规则是「Agent 一个提交都没产出就算失败」——但这个规则恰好惩罚了正确的行为(工作已经在前面的切片里做完了)。作者把它改成 ADR 而不是悄悄改代码,因为:

“If the agent silently does nothing (genuine bug, not "work already done"), the workflow still chains forward. The PR review step at the end of the PRD is the catch-all for "did this PRD actually deliver its acceptance criteria" — not the per-sub-issue commit count.”

如果 Agent 真的什么都没干(不是「活已干完」而是真 bug),流水线照样往前走。兜底的是 PRD 末尾的评审环节,而不是每个切片的提交数。

可迁移要点

ADR 对人类的价值是「可追溯」,对 Agent 的价值是「不许重新发明」。写 ADR 时最值钱的一节是 Rejected alternatives——它拦住的不是错误,而是「看起来更好的错误」。

07实践三:标准必须是 exit code,不是散文

CODING_STANDARDS.md 有 409 行散文。光靠散文,Agent 会读、会点头、然后照样违反。作者的答案是 19 个 check-*.sh / .ts 脚本(共 2623 行),把纪律编译成退出码:

check = check:static && test && check:client-build
check:static = check-effect-tsc && typecheck && lint && lint:boundaries
  && check-file-tokens   && check-no-dirname   && check-no-gitlinks
  && check-script-db-url && check-db-clients   && check-clip-inserts
  && check-env-example   && check-routes-folder && check-entity-links
  && check-duration-format && check-response-body && check-fs-writes
  && check-toast-import && check-effect-guards && check-frontend-state
  && check-background-jobs && check-complexity

这些守卫不是泛泛的 lint。它们编码的是这个项目的具体教训。看 check-toast-import.sh 开头的注释:

# Every toast goes through apps/local/app/components/ui/toast.tsx, which
# clamps its text, adds "Show more", and gives error toasts a "Copy" action.
# A toast imported straight from "sonner" skips all of that, so only the
# files that wrap sonner (and the wrapper's own test) may import it.

它甚至知道该放过哪些文件(三个白名单),并且报错信息里给了正确的替代方案:

ERROR: sonner imported directly — import toast from "@/components/ui/toast" instead:
"@/components/ui/toast bounds every toast (four-line clamp, Show more, Copy on errors).
For a caught error, use toastError(error, fallback) from the same module.

三个设计细节

  1. 每个脚本都支持 --all。默认是扫描 staged 文件(跑在 pre-commit 里,快);CI 里用 --all 扫全部。提交循环要短,CI 要全。
  2. 提交前的检查是 CI 的一半。文档明说:pre-commit 跑快速部分,故意省掉 check:response-body,「为了让提交循环保持短」。
  3. oxlint 是「建议」而非「阻塞」。原文:它的 correctness 警告是一份手工清理的存量待办,「你碰到的文件里有一条警告是邀请,不是拦路石」——只有那些编码了已记录标准的规则才是 error。这条区分极其重要:如果所有 lint 都是硬错误,Agent(和人)会学会绕过它。

还有一个更小的例子,说明这条路走得多远——一个 5 行的 hook,专门拦 npx tsc:

# .claude/hooks/block-npx-tsc.sh
if [ "$FIRST_WORD" = "npx" ] && echo "$COMMAND" | head -1 | grep -qE 'npx\s+tsc(\s|$)'; then
  echo 'Use `pnpm run typecheck` instead of `npx tsc`' >&2
  exit 2
fi

注释里还写着:只在 npx 真的是被执行的第一个词时才拦,不拦「提交信息里提到 npx tsc」这种文本参数。

可迁移要点

把「我们团队的规矩」翻译成 exit 1 的那一刻,它才对 Agent 生效。而且守卫的错误信息必须给出正确答案——Agent 读到「不许这样」会困惑,读到「改用这个」会照做。优先级:先守卫那些「错了会造成真实损害」的规则(写生产库、绕过 hook、直接 import 底层库)。

08实践四:用 GitHub label 当状态机

这是整个实践里最激进的一步:作者没有自建任务数据库。议题的状态完全由 label 表达,8 个 GitHub Actions 工作流监听 label 变化来推进。

docs/agents/afk-agent-platform-spec.md(65 KB)是这份设计的完整规格,开篇就声明了它的野心:

“This document is the source of truth. You should be able to re-implement the whole system in a fresh repository from this spec alone, without reading the original workflow YAML.”

这份文档是唯一事实来源。你应该能只靠它,在一个全新的仓库里把整套系统重建出来,而不需要去读原始的 workflow YAML。

label 就是状态

Label打在含义
agent:to-issuesPRD 议题把 PRD 拆成扁平的子议题列表
agent:implement议题同一个 label 触发两条不同流程——有子议题就走 PRD 模式,没有就走单议题模式
agent:queued议题可以干了,但被原生 GitHub 依赖阻塞着;阻塞者一关闭就自动提升
agent:in-progress议题一把锁:加上它属于「接受任务」的一部分,移除它写在 always() 里
agent:reviewPR评审并主动改进这个 PR
agent:blocked议题 / PR运行失败或被拒绝,需要人来看

注意 agent:implement 的歧义消解方式——按议题的「形状」判断,而不是靠人选择:

# 有子议题 → 这是 PRD,交给 agent-implement-prd.yml,这里静默跳过
# 有父议题 → 这是别人的子议题,拒绝运行(子议题从不直接打标签)
# 都没有   → 独立议题,正常实现

三条硬不变量

“One issue → one branch → one PR. Branch names are deterministic (agent/issue-<n>-<slug>)…
Nothing auto-merges to the base branch. Review marks a PR ready for review and posts a review with event: "COMMENT" — never APPROVE. A human approves and merges.”

第三条尤其值得注意:评审 Agent 发的永远是 COMMENT,从不 APPROVE。而且它不能关闭 review thread——「解决线程是评审者(人)的活,Agent 只能回复」。

为什么值得抄

用 label 当状态机的最大好处不是「省了一个数据库」,而是状态变更天然可审计、天然有 UI、天然能触发事件。你不需要写一套任务系统,GitHub 已经是了。代价是:并发、竞态、失败分类都得自己写进 workflow——这份规格里有专门的三节讲这个(§3.5 并发模型、§3.6 推送安全、§3.7 失败处理)。

09实践五:编排器独占变更权,Agent 只吐文件

整份规格里最重要的一句话,被作者称为「central design rule」:

“the orchestrator owns every tracker/VCS mutation (labels, comments, pushes, PR creation, issue closing). The agent runner only emits files. This seam is what makes the runner swappable and the system testable — the agent never holds a GitHub token and never calls the GitHub API to mutate state.”

编排器拥有所有 tracker / VCS 变更,Agent 运行器只输出文件。这条接缝让运行器可替换、让系统可测试——Agent 从不持有 GitHub token,从不调用 GitHub API 去改状态。

契约具体长这样:

输入

  • 环境变量:ISSUE_NUMBER、BRANCH、OUTPUT_DIR…
  • 预取好的上下文(diff、议题、PR 评论 JSON)
  • 一个已切到正确分支的 git 工作树

输出:OUTPUT_DIR 里的文件

  • failure_reason.txt — 失败原因
  • has_commits.txt — true/false
  • pr_title.txt、pr_description.txt
  • review_payload.json(经 schema 校验)

这个设计的收益是具体的:Architecture Review 工作流(每工作日定时跑一次,扫代码库找深化机会)也是只吐一份 PRD 的结构化输出,由编排器去建议题。作者明确写了:

“This holds universally — even Architecture Review only emits a PRD as structured output; the orchestrator creates the issue.”

可迁移要点

给 Agent 的 token 越少,它闯的祸越小;它和外部系统的接触面越小,你替换它时越轻松。把「做事」和「改状态」拆成两个进程,是你今天就能做的一个架构决定,与用不用 Claude Code 无关。

10实践六:produce / extract 两遍式,以及输入容忍

这是全篇最「懂 LLM」的一段。问题很具体:让一次 LLM 调用既干活又吐严格 JSON,很脆——一个格式错误的 blob 会让整次运行失败,而活已经干了(副作用已发生)。作者的两种解法:

有副作用的工作:produce(干活)+ extract(吐结构)两遍 第一遍 · produce 不带输出 schema 跑工作提示词 提交在这里产生 · 保留可续会话 id 一个坏的输出也中止不了这一次 第二遍 · extract 续同一会话:「把 output 吐出来,别改东西」 挂上 schema 校验 · 失败就带反馈重试 最多重试 N 次(默认 3)· 不重做工作 续会话 无副作用的工作(起草 PR 标题、拆分 PRD)→ 一遍即可:runWithRetry 输出本身就是工作产物:schema 校验失败就续会话重吐,最多 N 次,不重做任何东西。 判据只有一条: 提交和评论是不能重复的副作用 → 必须两遍;起草文本可重来 → 一遍。
规格原文:「Produce 阶段跑工作提示词,不带输出 schema,这样一次坏的吐出就无法中止它;Extract 阶段续会话再吐结构。」

输入容忍:模型会漂移

更见功力的是作者对「模型输出不稳定」的处理方式——不是加更多约束,而是在 schema 里接受别名:

最后一条是关键:不指望模型不犯错,而是在落库前用真实 diff 做一遍交叉验证。规格里那句小标题叫 Input tolerance (hard-won robustness)——「hard-won」两个字,说明这是踩出来的。

可迁移要点

两件事今天就能做:(1)凡是「有副作用 + 要结构化输出」的 Agent 任务,拆成两遍;(2)凡是让模型产出「指向真实代码的位置」的输出(文件名、行号、issue 编号),解析后一律用真实数据做一次存在性校验再落地。这两条能把 Agent 流水线的失败率砍掉一大半。

11实践七:垂直切片,且切片必须扁平

.claude/skills/to-issues-project/SKILL.md 负责把 PRD 拆成子议题。它的核心规则叫 tracer-bullet(曳光弹):

“Each slice delivers a narrow but COMPLETE path through every layer (schema → API → UI → tests), NOT a horizontal slice of one layer…
Sub-issues are flat — a sub-issue must not itself need sub-issues. If a slice is too big to leaf, split it into multiple peer slices instead of nesting.”

配套的几条约束都很实在:

模板里还有一处微妙的安排:子议题正文故意不包含 Closes 指令。关闭子议题是 PRD 模式工作流的职责,关闭 PRD 本身靠 PR 描述里的 Closes #<PRD>。谁关什么,是设计好的,不能让 Agent 顺手关错。

可迁移要点

「垂直切片」是老概念,但对 Agent 有新增量:切片必须能独立验收,因为 Agent 没有跨会话的隐性记忆——它只看到分支上有什么。另外「正文不写文件路径」这条反直觉但正确:路径会过期,而决定不会。

12实践八:验收必须是真实浏览器自证,而不是「测试绿了」

这是我认为整个仓库最值得抄的一份文件:.claude/skills/verify-cvm/SKILL.md,417 行。它解决的是一个所有人都有的痛点——Agent 说「我做完了,测试通过了」,但 UI 根本没生效。

作者把它写成了硬性流程,塞在 CLAUDE.md 的最显眼处:

“Any change with a visible effect in the UI is checked in the browser with verify-cvm before you open the PR. Passing tests and a green check do not replace it… The PR body states what you clicked and what you saw, with screenshots or the evidence path. A change with no UI effect says so in one line — No UI change — instead.”

任何对 UI 有可见影响的改动,在开 PR 之前都要用 verify-cvm 在浏览器里跑一遍。测试通过和 check 变绿不能替代它。PR 正文要写清楚你点了什么、看到了什么,附截图或证据路径。没有 UI 影响的改动就写一行「No UI change」。

最后那半句是点睛之笔:「没有 UI 改动」也必须显式声明。不给 Agent 留「我也不知道算不算」的模糊地带。

verify-cvm:每一次运行都是一个与世隔绝的沙盒 本次运行的沙盒 · 从模板库克隆出的专属数据库(用完即删) · 先应用本分支缺的迁移,再启服务 · 端口锁在 5200–5299(CVM 本人用 5170–5199) · 文件写入落在 scratch/,永不碰作者磁盘 · 自带 sidecar 跑后台任务(GPU 上的 Whisper) · 全程留证:Write Ledger 记录每一次写 生产数据库 默认永不可达 显式要也只读 外部服务 凭据全部换成废值 或指向丢弃端口 作者正在录制? 每一步前自动检查 在录就打印 PAUSED 并等 真实录制素材 服务不读 /mnt Footage 显示「已拒绝」 Write Ledger · 写 ledger 不许靠猜 两条精确来源:语句级触发器 + 应用的 SQL 日志 按「谁写的」分:服务 / 其他连接 / sidecar 后台 三种结论之外无「干净」:PENDING / UNKNOWN / 写入 点一个按钮却写了三张表 = 这次运行抓到的一个 bug
「写 Ledger 从不靠猜」——技能文档里专门记录了它曾经误判过:早先读 pg_stat_user_tables 计数器(有延迟),把一次真实的 UI 保存判成了「干净」。

三个我觉得最狠的细节

  1. 它连「作者正在录视频」都知道。每个动作前自动调用 is-filming.sh,在录制就打印 PAUSED、每 60 秒复查、自己继续。「别绕过暂停」——因为录制时抢 GPU 会毁掉素材。
  2. 它拒绝自己造凭据,也拒绝自己更新快照。模板库缺失时的指令是:「停下来问 Matt」,并且明令禁止 Agent 自己跑 db:verify-snapshot(那是唯一读生产的一步)。两个脚本都会拒绝 Agent 的 shell。
  3. 它承认自己的盲区。功能地图 features/README.md 末尾老实写着哪些页面「还没画进来」(Diagrams、归档课程、API Token…),以及 6 个已映射功能的多个入口。原文:「在你宣布某功能已验证之前,先查查这个索引有没有列出同一个行为的另一个入口。」

证据的定义也被写死了四条:走真实用户路径(路由 + 按钮,不是自己搓一个 API 调用)、拍动作前和动作后两个状态、验证副作用(回读那一行,而不只是看页面)、看控制台(水合失败的截图是看不出来的)。

可迁移要点

不需要照抄这套 417 行的机器,值得抄的是三条原则:(1)「测试绿了」不等于「功能生效」,必须有独立的、看得见 UI 的验收环节;(2)给 Agent 的数据永远是克隆,且物理上断掉它通往生产的一切路径(不是靠提示词请求,是靠代码拒绝);(3)验收要留下人能不重跑就能读懂的证据。

13量化复盘

规模与节奏

指标数值说明
提交总数3534其中 940 个是合并提交,2594 个是实质提交
时间跨度15.4 个月2025-07-23 → 2026-10-30
AI 署名提交1961占实质提交 75.6%;含合并提交口径则是 56.5%
RALPH: 前缀提交766无人值守批处理循环产物,2026-06 后归零
合并的 PR483平均每个 PR 约 5.4 个实质提交
提交粒度中位 9 文件均值 14.6;59% 的提交 ≤10 个文件;最大 184
峰值日325 提交2026-10-09 单日,其中大量是并行 worktree 的合并
代码 / 测试行347k / 126k653 个测试文件,测试密度约 36%
提交粒度:近 300 次提交触及的文件数分布 ≤ 3 个文件 15% ≤ 10 个文件 59% 中位数 = 9 个文件 均值 14.6 最大一次触及 184 个文件(批量重构,如 reducer 迁移) 粒度偏大说明:Agent 倾向一次做完一个完整切片(垂直切片规则的直接后果),而非逐文件提交。
样本:最近 300 次提交(含合并提交)。git show --stat 逐条解析。

模型谱系:一年换了 10 种配置

署名 trailer 里留下了完整的模型演化痕迹,说明作者一直在换模型,而不是绑定某一个:

模型提交数说明
Claude Opus 4.6862主力,横跨 2026 年上半年
Claude Opus 56112026 下半年主力
Claude Opus 5.5544当前默认(merging.md 指定每个提交都带它的署名)
Claude Opus 4.52382026 年初
Claude Sonnet 4.595轻量任务
Claude Opus 4.8 (1M)681M 上下文版本
Claude Sonnet 4.660轻量任务
Claude Opus 4.7 (1M)441M 上下文版本
Claude Opus 5 (1M)371M 上下文版本
Claude Opus 4.6 (1M)221M 上下文版本

口径说明:同一提交可能匹配多个模型名(如「Opus 5」是「Opus 5 (1M」的前缀),所以上表之和大于 1961。趋势可放心引用,绝对数请按「主要型号」理解。

14代价与风险:老实说

风险一:AI 写实现,也写测试 —— 测试证明的只是「实现与实现者的理解一致」

DB-TDD.md 里那条「不要测试类型系统已经证明的东西」是对的,但它无法解决根本问题:当一个 Agent 同时写了实现和测试,绿灯只能证明这两份代码彼此一致,不能证明它们都符合业务意图。

这个项目的缓解手段是 verify-cvm(真实浏览器)+ 人评审 PR,两者都在 Agent 之外。如果你的项目没有这两层,TDD 的绿灯基本是安慰剂。

风险二:整套系统的可用性押在「一个人维护的文档」上

4809 行契约 + 65 KB 平台规格 + 417 行验证技能 + 19 个守卫脚本——这些是单人维护的基础设施。规格里那些「hard-won robustness」的注释说明:每一条都是踩坑换来的,而踩坑的人只有作者一个。

对团队而言这是真实的复制壁垒:不是技术门槛,是投入门槛。合理的做法是挑 ROI 最高的三条先做(见下节),而不是一次性照抄。

风险三:oxlint 警告是「邀请而非拦路石」,意味着存量技术债在缓慢累积

作者明说 correctness 警告是一份手工清理的存量待办。这个政策对 Agent 友好(不会因为碰了一个文件就被迫清理整个文件),但代价是债务不清零。在 347k 行代码上,这会变成一个长期负担。

对策:给存量债务设一个硬上限(如「警告数不得增长」),把它变成一条机械规则,而不是靠自觉。

风险四:谷底暴露的能力边界 —— 决策未定型时,AI 参与率腰斩

2026-08/09 的 36% 不是偶然:那两个月在写 8 份 ADR 并攻坚 Animatic 视觉分镜。凡是「还没决定怎么做」和「需要肉眼审美判断」的领域,Agent 参与率会塌。这不是缺陷,是当前的真实边界。

对策:把这类工作显式标记为「人工期」,别指望自动化;同时注意——正因为 ADR 在这两个月定型了,10 月的 AI 参与率才能回到 89%。决策投入不是自动化的对立面,是它的前置条件。

局限:本报告的方法学边界

本次分析没有作者的 Claude Code 会话日志(~/.claude/projects/ 下没有本项目目录),所以无法统计 token 消耗、会话时长、人类指令条数。所有结论来自仓库内的 AI 契约文件 + git 元数据 + 代码三方交叉取证。这比会话日志更可靠地反映了「沉淀下来的实践」,但无法反映「实际执行中的偏差」——例如 Agent 有多少次违反了守卫又被拦下。

15可复用清单(按投入产出比排序)

#做什么投入为什么值 / 怎么起步
1写一个带 _Avoid_ 的术语表半天最便宜、见效最快。20 个核心概念就够。 Agent 立刻停止造同义词。
2把「不许做」写成守卫脚本1—2 天先守三条最痛的(写生产库、绕过 hook、直接依赖底层库)。错误信息必须给出正确替代。
3「测试绿了」之外加一道 UI 自证2—3 天不必做 417 行。最低配:一条命令起隔离环境 + 强制 PR 正文写「我点了什么、看到了什么」。
4ADR 必须写 Rejected alternatives边际成本改模板即可。它拦住的是「看起来更好的错误」——最贵的一类返工。
5有副作用的 Agent 任务拆成 produce / extract1 天今天就能改。凡是「先干活再吐 JSON」,都拆成两遍。失败率立刻下降。
6模型产出的「位置」一律用真实数据校验半天文件名 + 行号 + issue 编号,落库前拿真实 diff 过一遍,不存在就丢。
7Agent 不持有写权限,只吐文件架构决定编排器独占所有远端变更。换模型 / 换 runner 时零改动。
8垂直切片 + 扁平子任务边际成本切片要能独立验收、一次会话能做完。禁止嵌套。
9永不自动合并;Agent 永不 APPROVE零成本一条规则,守住最后一道闸。也别让它关闭 review thread。
10统计 AI 参与度时先剔除合并提交5 分钟否则你的数字会低 20 个百分点,然后得出错误结论。

16证据索引与复算命令

本报告的每一个数字都有一条命令可以复跑。分析过程全程只读,未对被分析仓库做任何修改。

# 克隆
git clone https://github.com/mattpocock/course-video-manager.git
cd course-video-manager

# 总提交 / 合并提交 / 实质提交
git log --oneline | wc -l                       # 3534
git log --merges --format=%H | wc -l            # 940
git log --no-merges --format=%H | wc -l         # 2594

# AI 参与度(两个口径,注意差 20 个百分点)
git log --grep='Co-Authored-By: Claude' --format=%H | wc -l                    # 1995 (56.5%)
git log --no-merges --grep='Co-Authored-By: Claude' --format=%H | wc -l       # 1961 (75.6%)

# RALPH 无人值守循环
git log --grep='^RALPH:' --extended-regexp --format=%H | wc -l                # 766

# 按月聚合(本文图二的数据来源)
git log --no-merges --date=format:'%Y-%m' --format='%H %ad'      # 分母
git log --no-merges --grep='Co-Authored-By: Claude' \
        --date=format:'%Y-%m' --format='%H %ad'                   # 分子

# 模型谱系
for m in "Opus 4.6" "Opus 5" "Opus 5.5" "Opus 4.5" "Sonnet 4.5"; do
  echo "$m $(git log --grep="Co-Authored-By: Claude $m" --format=%H | wc -l)"
done

# 规模
git ls-files | grep -E '\.(ts|tsx)$' | xargs wc -l | tail -1      # 347284
git ls-files | grep -E '\.(test|spec)\.(ts|tsx)$' | wc -l         # 653 个测试文件
ls docs/adr/ | wc -l                                              # 56 份 ADR
ls scripts/ | grep -c '^check-'                                   # 19 个守卫脚本

# PR 数
git log --format='%s' | grep -c '^Merge pull request'             # 483

# 契约体量
cat CLAUDE.md CODING_STANDARDS.md GLOSSARY.md docs/agents/*.md \
    $(find .claude/skills -name '*.md') $(find .sandcastle -name '*.md') | wc -l   # 4809

关键文件路径索引

路径行数它证明了什么
CLAUDE.md81入口导航;UI 改动必须浏览器自证的硬要求
GLOSSARY.md578词汇即接口;每个概念的 _Avoid_ 反义词表
CODING_STANDARDS.md409工程纪律散文层(再由脚本编译成 exit code)
docs/agents/afk-agent-platform-spec.md65 KBAFK 平台完整规格;可据它在新仓库重建整套系统
docs/agents/merging.md~60落地一个改动的 5 步;「永不跳过 hook」
docs/agents/domain.md~50ADR 冲突必须显式声明,不许静默覆盖
docs/agents/triage-labels.md~405 个 triage 角色 → 本仓库实际 label 的映射表
docs/agents/queued-promotion.md~60竞态处理;label 触发下游需要 PAT 而非 GITHUB_TOKEN
.claude/skills/verify-cvm/SKILL.md417真实浏览器自证;隔离边界;Write Ledger
.claude/skills/verify-cvm/features/*.md6 个功能地图;老实列出「还没画进来」的页面
.claude/skills/do-work/{DB,FRONTEND}-TDD.md56一次只写一个失败测试;不测类型系统已证明的东西
.claude/skills/to-issues-project/SKILL.md136tracer-bullet 垂直切片;子议题必须扁平
.claude/skills/to-prd-project/SKILL.md86PRD 是 spec 不是 sketch;四类读者
.claude/skills/deploy-migration/SKILL.md71生产迁移只由人执行;破坏性变更走两段式
.claude/skills/optimize-loader/SKILL.md79性能反模式(重复取数 / 过度取数 / 串行)
.claude/hooks/block-npx-tsc.sh5最小可行守卫:拦 npx tsc,且只拦真执行
.sandcastle/*/prompt.md9 个提示词即流程操作系统;红-绿-重构写进提示词
.github/workflows/agent-*.yml7 个label 状态机的实际实现;形状探测 / 前置检查
scripts/check-*.{sh,ts}19 个 / 2623 行把纪律编译成退出码
plans/ralph.sh~25第一代自动化:无人值守批处理循环(已被取代)
docs/adr/0008-agent-implement-zero-commit-runs.md~30系统性自我纠错;Rejected alternatives 的范本

一句话总结

这个项目的 AI 原生程度,不是由「用了多少 AI」决定的,而是由「有多少工程判断被写成了机器可读、机器可执行的文本」决定的。 75.6% 的提交由 AI 产出只是结果;4809 行契约、56 份 ADR、19 个守卫脚本、417 行验收技能才是原因。 而 2026 年 8—9 月那个 36% 的谷底提醒我们:在决定还没做出来之前,什么都自动化不了。