CVM · AI 原生复盘 目录
只读取证 · 单文件报告

course-video-manager:一套已经跑到生产里的 AI 原生工程体系

作者 Matt Pocock 把一个 34 万行的视频课程管理单体,改造成了「贴标签 → GitHub Actions 编排 → 沙箱里的 Claude Code 动手 → 人只审批合并」的自治流水线。 这份报告通读全仓 2506 个文件、3534 次提交与 56 篇 ADR,把它做了什么、为什么这么做、以及哪些可以照搬到你的项目里,逐条落到文件路径上。

仓库 mattpocock/course-video-manager 取证方式:本地克隆 + 只读分析 证据:全部标注文件路径

01一页看懂

3,534
总提交(2025-07-23 → 2026-10-30)
62%
携带 Claude 协作署名的提交占比
2200 / 3534
100%
2026-10 非合并提交的 AI 参与率
573 / 573
19
静态守卫脚本(13 sh + 6 ts)
56
篇 ADR(Architecture Decision Record)
134
GLOSSARY.md 术语条目
8
个智能体 workflow(标签驱动)
170
份白名单里的放行命中(只减不增)

这套体系到底在跑什么

一句话:人负责选题、定规范、审批合并;机器负责实现、自我审查、修冲突;二者之间用「文件」而不是「对话」交接。

端到端自治流水线 · 箭头上的数字 = workflow 编号 人类 / 定时 label agent:* ① To Issues PRD → 子 issue ②③ Implement 沙箱内写代码 Draft PR 永远不会自动合并 ④ Review(智能体自审改进) event: COMMENT,永不 APPROVE ⑤ Implement PR · ⑥ Update Branch 按人的评论返工 / 解冲突 ⓗ 人类读 review → 审批 → 手动 merge(唯一入口) --admin / 规则集绕过 / 直推 main 均为人类专属 merge 到 main check 绿 + 分支最新 Closes #N 触发 issue closed 事件 ⑦ Promote Queued 阻塞清零 → 自动开工 回到 ② 循环 闭环 硬不变量(写在规范 §3.9,不是建议而是必须) 1. 智能体从不持有 GitHub token,也不调用 API 改状态 2. 任何东西都不会自动合并到 main 3. 智能体不能关闭自己的 review 线程;一个 issue 一条分支一个 PR
图 1 端到端自治流水线。依据 docs/agents/afk-agent-platform-spec.md §2 的 mermaid 图与 §3.9 重绘制。

三个反直觉的事实

反直觉 1

「AI 没写一行代码」也可能是成功

ADR 0008 明确把「零提交的智能体运行」从失败改判为成功:PRD 的子 issue 之间不独立,第 4 个子 issue 描述的活儿往往已经被第 2 个子 issue 那次提交做掉了。旧启发式 result.commits.length === 0 → fail 曾真的炸过(PRD #853 / run 26230340854)。

docs/adr/0008-agent-implement-zero-commit-runs.md
反直觉 2

「manual」不等于「人来敲」

ADR 0022 把 Deliverables 日历从「人工」重新定义为「非派生(underived)」:智能体通过 cvm deliverable create/update/archive 写日历,与人在 UI 里编辑是同一种刻意创作行为。它甚至明知 archive 不可逆(全产品无任何入口能撤销),仍然开放给智能体——因为不给的话,智能体会用 cancelled 当软删除,从而污染状态语义。

docs/adr/0022-deliverables-are-agent-writable.md
反直觉 3

侵入式禁掉的恰恰是「聪明做法」

写给智能体的测试文档里明令:不许用 turbo --affected 或 vitest --changed 去猜「这个 diff 可能破坏了哪些测试」。理由:这种 git-diff 启发式会退化成跑整个包,比手动指定路径更慢。

docs/agents/testing.md:19

02方法与口径

本报告对 /Users/junjian/WorkBuddy/2026-10-31-07-17-08/course-video-manager 做只读分析,未在被分析仓库中写入或修改任何文件。

取证来源

  • 仓库全量文本通读:2506 个非 git 文件
  • git 历史:3534 次提交(git fetch --unshallow 后)
  • 核心约定文件、ADR、CI workflow、沙箱脚本、守卫脚本逐句精读

几个声明

  • 「AI 参与率」= 提交 trailers 含 Co-Authored-By: Claude … 的比例,这是下限而非上限(人工润色过的 AI 提交可能不带 trailer)。
  • 代码行数口径:find apps packages -name "*.ts*"(不含 node_modules/dist)= 341,650 行。
  • 仓库中所有作者信息都是 Matt Pocock,署名分布无法区分人机,因此只用 trailer 作代理指标。
复算命令(可自行验证本报告的每一个数字)
cd course-video-manager
git fetch --unshallow            # 浅克隆默认只有 50 个 commit

# 总量与时间跨度
git log --oneline | wc -l
git log --reverse --pretty=format:'%ad' --date=short | head -1

# AI 署名提交数
git log --format='%(trailers:key=Co-Authored-By,valueonly)' | grep -c Claude

# 按月占比(去掉合并提交)
for m in 2026-03 2026-10; do
  tot=$(git log --no-merges --date=format:'%Y-%m' --pretty=format:'%ad' | grep -c "^$m$")
  cl=$(git log --no-merges --date=format:'%Y-%m' \
        --pretty=format:'%ad|%(trailers:key=Co-Authored-By,valueonly)' | grep "^$m|" | grep -c Claude)
  echo "$m $cl/$tot"
done

# 代码行数 / 测试数 / ADR 数 / 守卫数
find apps packages -name '*.ts' -o -name '*.tsx' | grep -vE 'node_modules|dist' | xargs cat | wc -l
find . -name '*.test.ts'      -not -path '*/node_modules/*' | wc -l
find . -name '*.browser.test.tsx' -not -path '*/node_modules/*' | wc -l
ls docs/adr | wc -l
ls scripts | grep -c '^check-'                       # 19
ls scripts | grep '^check-' | grep -c '\.sh$'        # 13
ls scripts | grep '^check-' | grep -c '\.ts$'        # 6

# 白名单放行命中数
node -e "const j=require('./scripts/effect-guards-allowlist.json');\
  let c=0;for(const k of Object.keys(j)){const v=j[k];\
  c += Array.isArray(v)?v.length:(typeof v==='object'?Object.keys(v).length:v);}console.log(c)"   # 25

03实践一:把「智能体契约」做成三层金字塔,顶层只留 81 行

这个仓库没有 AGENTS.md,也没有任何 cursorrules / .windsurfrules / Copilot 配置文件——全仓搜索无结果。 它对「多份约定文件互相漂移」这一现象的答案不是「同步它们」,而是只承认一份:CLAUDE.md。

L1 · 常驻上下文(每次会话都在) CLAUDE.md — 81 行 / 11.4 KB 只放指针与硬约束,正文一律下推 L2 · 按需加载(干活前被告知去读) docs/agents/*.md · CODING_STANDARDS.md merging / testing / domain / triage-labels / queued-promotion / backlog L3 · 领域世界模型(写任何代码前必读) GLOSSARY.md 578 行 / 134 条 · docs/adr/ 56 篇 统一词汇 + 被冻结的决策;缺失则静默继续,不要求补写
图 2 契约分层。顶层是唯一必读文件,其余全部通过显式指针按需加载。

L1:CLAUDE.md 是一份「指针清单」而不是「百科全书」

81 行里没有一行在教怎么写代码,全是在说「去读哪」和「绝对不能做什么」。几个典型的两句式:

章节原文要点(依据 CLAUDE.md)
Agent skills / BacklogIssue 与 PRD 就是 GitHub issue,通过 gh CLI 管理,详见 docs/agents/backlog.md
Landing a change「每次改动都必须从干净的 worktree 出发,PR 只有在分支与 main 同步且 check 绿时才合并。开工前和合并前各读一遍 docs/agents/merging.md」
Verifying a change「任何在 UI 上看得见的改动,在开 PR 之前必须在浏览器里用 verify-cvm 跑一遍。测试通过和 check 变绿都不能替代它。没有 UI 变化的改动就写一行 No UI change」
Coding standards「写代码时读,不是只在评审时读——『每个 any 都是一处泄漏』这句话约束的是写那个 cast 的手。它放在仓库根目录而不是 .sandcastle/,正是为此」
cvm CLI「只有一种传输,作者调用和智能体调用共用;不许加进程内 fallback。所有 import []的检查一个都逃不掉」
证据:CLAUDE.md(81 行,仓库根目录)
可迁移要点 CLAUDE.md 的价值不在「写得多全」,而在「把长文档变成按需查阅的指针」。81 行意味着它每次都会完整地挂在上下文里,而 CODING_STANDARDS.md 的 410 行只在真的写字时才被拉进来。把体积最大的内容放在最不需要常驻的层级,是这个三层结构的全部秘密。

L2:docs/agents/ 是「给机器的操作规程」,和给人看的 README 完全分离

注意这个目录的命名:不是 docs/dev/,而是 docs/agents/。七份文件,每份讲一件事,最大的一份是 1092 行的 AFK 平台规范。

文件行数职责
afk-agent-platform-spec.md1092整个自治平台的可实现规范,自称 source of truth
merging.md23五步落地法 + 轮询纪律 + 「永不使用 --no-verify」
testing.md36本地定向 / CI 全量两档制
domain.md51动工前先读哪些领域文档;文件不存在就静默继续
triage-labels.md28技能里的规范标签 ↔ 本仓库真实标签的映射表
queued-promotion.md40依赖解除后自动晋升的状态迁移规则
backlog.md22gh CLI 的调用约定
prompts/*.prompt.md8 份各 workflow 注入智能体的提示词模板

其中最见功力的是 triage-labels.md:技能(skill)里说的是「打上 ready-for-agent 标签」,本仓库里这个标签拼作 Sandcastle。它不要求改技能、也不要求改仓库,而是用一张 6 行的映射表在中间翻译:

技能里的规范名本仓库真实标签含义
needs-triageneeds-triage维护者需评估
needs-infoneeds-info等信息
ready-for-agentSandcastle规格完备,可以交给离线智能体
ready-for-humanready-for-human必须人来做
wontfixwontfix不做
证据:docs/agents/triage-labels.md(「Edit the right-hand column to match whatever vocabulary you actually use.」)

L3:先有统一词汇,才有能交流的智能体

GLOSSARY.md 不是文档,是给人和机器共享的概念表:578 行、134 个术语条目,每个条目除了定义还明确列出要避开的同义词。摘一条:

Lesson: A single learning unit within a Section. A Lesson is known by its title, which is unique among its siblings.

Avoid: Exercise, Tutorial, Step

GLOSSARY.md · Course structure 一节

这条约束被直接写进了智能体干活时的执行层:improve-codebase-architecture-project 技能要求「领域语言必须用 GLOSSARY.md 的词汇」,而 domain.md 更进一步规定——当你的输出(issue 标题、重构提案、测试名称)里出现某概念时,必须用术语表的叫法。

这里有一个被很多团队忽略的细节 docs/agents/domain.md 第 11 行写着:如果 GLOSSARY.md / docs/adr/ 不存在,就静默继续,不要报错、不要建议创建。
理由:术语和决策是被实际解决的问题催生出来的,不是预先设计出来的。让智能体在缺文件时不啰嗦,是这套体系能长期不被文档债拖垮的前提。

04实践二:一条由 GitHub 标签驱动的 AFK 流水线

AFK = Away From Keyboard。作者写的不是「一个 Claude Code 用法 tips」,而是一份能在全新仓库里照着重建整套系统的规范:

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.

docs/agents/afk-agent-platform-spec.md:9-11

四大支柱,只留「一个可插拔轴」

支柱状态承担什么
编排器 Orchestrator固定 GitHub Actions触发、权限、并发组、token
追踪器 Tracker固定 Issues + PRs标签即状态、原生子 issue、原生依赖阻塞关系
版本控制 VCS固定 git + GitHub分支、force-with-lease、draft PR、Closes #N
智能体运行器 Runner可插拔吃 env 变量 + 上下文 → 在 checkout 里干活 → 往目录里写文件

八个 workflow:标签即状态机,没有外部数据库

agent:to-issues 人类贴(PRD) agent:queued 人类贴(有阻塞) agent:implement 「开始干活」 agent:in-progress 既是状态也是分布式锁 agent:review PR 等待自审 blocked 需人处理 人类 merge PR ready → 审批 agent:update-branch 合并主干 / 解冲突 主链:to-issues → implement → PR → review → 人 merge → Closes #N → 下一个 queued 自动开工
图 3 agent:* 标签状态机(本体可有)。依据 afk-agent-platform-spec.md §3.2 与 §2 的 stateDiagram 重绘制。
Workflow触发并发组权限
agent-implement.ymlissue 贴 agent:implement按 issuecontents/issues/PR write
agent-implement-prd.yml同上(有子 issue 时走这条)按 issuecontents/issues/PR write
agent-implement-pr.ymlPR 贴 label按 PRcontents/PR write(无 issues)
agent-review.ymlPR 贴 agent:review按 PRcontents/PR write
agent-update-branch.ymlPR 贴 label按 PRcontents/PR write
agent-to-issues-prd.ymlissue 贴 agent:to-issues按 issuecontents: read + issues write
agent-promote-queued.yml任意 issue 关闭— 刻意不设issues write
architecture-review.yml工作日定时 0 9 * * 1-5architecture-reviewcontents read + issues write

三个值得抄走的机制细节

1
所有并发组都设 cancel-in-progress: false——一次智能体运行很贵,绝不能因为来了个新事件就被取消;后来的排队等着。afk-agent-platform-spec.md §3.5(唯一定时任务和 test.yml 是 cancel-in-progress: true)
2
三种 push 策略按场景区分:单 issue 用 --force(分支名由 issue 号决定,残留必是孤儿);PRD 用普通 push(「NOT force-push: the branch accumulates commits across sub-issue runs」);review/改 PR 用 --force-with-lease="refs/heads/$BRANCH:$BRANCH_HEAD_SHA" 做租约检测。afk-agent-platform-spec.md §3.6
3
pull_request_target 而不是 pull_request:后者依赖 GitHub 生成的 merge commit,PR 落后或有冲突时根本生成不出来,labeled 事件就不会触发;前者在 base context 里跑,不需要 merge commit。.github/workflows/agent-review.yml:3-8 注释
本地还有另一条线:RALPH 循环 CI 之外,仓库里保留了plans/ralph.sh 与 plans/backlog/{affk,once,setup,cleanup}.sh,跑的是 Docker sandbox 版 Claude Code,最多 10 轮 × 4 并发,配合 plans/backlog/prompt.md。
这条「土味」循环留下了一个很实用的提示词约束:提交信息必须以 RALPH: 前缀,并在 commit message 里写清「完成的任务 + PRD 引用 + 关键决策 + 改动文件 + 给下一轮的阻塞提示」。而 CI 侧的 .sandcastle/implement/prompt.md 则明确写着 「Do NOT use a RALPH: prefix — that prefix is reserved for the RALPH loop」。两条流水线共用一套仓库,但提交历史必须能分辨来自哪条线。 plans/ralph.sh · plans/backlog/prompt.md:33-50 · .sandcastle/implement/prompt.md:29

05实践三:人机分工边界是用「权限」而不是「礼貌」划定的

大部分团队写「人机协作」时说的是态度;这个仓库说的是谁拿什么 token、能调用哪个动词。

3.1 中央设计法则:编排器持有所有写权限,运行器只产出文件

The 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.

docs/agents/afk-agent-platform-spec.md:27-30

这条被划得有多干净?看实施提示词:.sandcastle/implement/prompt.md 最后一行是 「Do not close the issue yourself.」——连关闭自己刚做完的 issue 都不行,因为那是一次 tracker 变更。

3.2 一张边界总账

动作智能体编排器人类依据
写代码 / 跑测试 / 本地提交✅——implement/prompt.md
git push 到分支—✅—§3.6 / agent-implement.yml:166
创建 PR—✅ draft—agent-implement.yml:184-189
给 PR 写 review✅✅ 投递✅review.ts:223
review 结论为 APPROVE❌ 永不—✅§3.9 non-goals
关闭自己的 review 线程❌ 只能回复—✅§3.9 hard invariant
合并到 main❌❌✅ 唯一merging.md:17
改写 .github/workflows/需 PAT✅—agent-implement.yml:104-108
对生产库做 db:migrate❌—✅ MattADR 0026 · README.md:68
跑 db:verify-snapshot(唯一读生产的快照)❌ 脚本拒绝—✅ Mattverify-cvm/SKILL.md:82-87
Matt 录制时启动重进程(ffmpeg / OBS 争抢)❌ 自动暂停——verify-cvm/SKILL.md:12-20

3.3 三个 token 的分工与「降级契约」

# Use AGENT_PAT so pushes can include changes to files under
# .github/workflows/ (GITHUB_TOKEN lacks the `workflows` scope).
# Falls back to GITHUB_TOKEN if AGENT_PAT is unset; in that case
# workflow-file changes will be rejected on push.
token: ${{ secrets.AGENT_PAT || secrets.GITHUB_TOKEN }}
.github/workflows/agent-implement.yml:104-108

降级路径也很明确:先试 PAT,失败打印 AGENT_PAT label add failed …; falling back to GITHUB_TOKEN.——此时标签会落地,但下游 workflow 不会自动触发,得人手动再贴一次。这是被规范(§3.4 Degradation contract)承认并写死的行为,不是意外。

最有意思的一条边界:智能体不能妨碍真人干活 verify-cvm 会在每个步骤前检查 scripts/is-filming.sh——如果 Matt 正在录制视频,所有会启动浏览器的动词都会打印 PAUSED 并每 60 秒重查一次,不允许绕过。这是一台真机器上跑的和真 GPU、真摄像头共享资源的项目,人机边界不只是一条代码规则,还是一条物理资源规则。
同一思路下还有 CODING_STANDARDS.md 的 Windows 进程守卫:启动任何 .exe 前必须过 guardWindows(),而它在作者的录制软件运行时会直接拒绝,并把进程强制跑在 Idle 优先级 + 6-9 号核上。 .claude/skills/verify-cvm/SKILL.md:12-20 · CLAUDE.md §Windows processes

06实践四:类型系统抓不到的,就写一个脚本去抓

这套体系里最锋利的一句话,藏在一个 30 行的 shell 脚本注释里:

Eight routes had this, each one copied from the last. No type could catch it, because the cast is what turned the type off. So it is caught here.

scripts/check-response-body.sh:14-15(背景:issue #1722,8 个路由照抄同一个把 ReadStream 塞进 Response 的模式,整个 server 崩溃)

翻译成 workings:当 AI(或人)用 as any 绕过类型系统时,类型检查就失效了——于是他们为每一次失效打一个补丁脚本。

19 个守卫,每个都拦一件具体的事

脚本拦什么白名单
check-response-body.shcreateReadStream 直接进 new Response(),以及 Response 内的 as any 转换—
check-effect-guards.tsEffect 三种逃逸:边界外 runPromise / 吞错误的 catchAll / 产品码里 Effect.promise25 命中
check-frontend-state.ts单文件 useState > 4,或 effect 回调里引用本文件 setter > 1 次(藏在 useEffect 里的状态机)6 命中
check-background-jobs.ts4 类「后台活儿跑进请求里」:SSE 路由、浏览器驱动、spawn、出网调用(fetch/AI SDK/OpenAI…)39 命中
check-complexity.ts圈复杂度 > 15;且白名单不得比 origin/main 更高100 文件 / 110 命中
check-file-tokens.sh单文件超过 5500 token(按字节/4 估算),逼迫拆模块12 排除
check-fs-writes.sh对「应用自己没拼出来的路径」做写/删/改/拷——必须先过 assertUnder9 文件
check-db-clients.sh绕过连接守卫自己造 Pool/Client/drizzle(),或自己读 DATABASE_URL10
check-clip-inserts.sh绕过 COPY_PATHS 手写 Clip 行插入(会漏带 Transcript Words / Overlays)7
check-no-gitlinks.shindex 里的 gitlink(mode 160000)——智能体 worktree 被 git add -A 卷进来的经典事故—
check-routes-folder.sh把 *.test.ts/__tests__/ 放进 app/routes(那里每个文件都是公网路由)—
check-env-example.sh代码读了某个 env,但 .env.example 里没写(注释:Do not add one.)4 known gaps
check-no-dirname.shESM 项目里的 __dirname / __filename—
check-script-db-url.sh脚本自己读 DATABASE_URL,强制走 scriptDatabaseUrl()—
check-effect-tsc.tstsc 补丁丢失——用金丝雀文件断言 effect(floatingEffect) 确实被报出来,否则 Effect 规则已静默失效—
check-entity-links.sh手写 deep link(clipboard.writeText(id)、拼 URL),强制走 entityDeepLink—
check-duration-format.sh手写时长格式化(同文件同时出现 % 60 和 padStart(2)—
check-toast-import.sh除 3 个 wrapper 外任何文件 from "sonner"3
lint:boundaries(dep-cruise)packages/core 触碰文件系统/子进程/git(core-is-filesystem-free,23 个模块模式)仅测试文件

关键机制:所有技术债都用「只减不增」的白名单编码

这是我认为整个仓库最值得照搬的一条。守卫落地时,先把历史欠债记进白名单;之后用「1+1+1=3」条机器校验逼债:

// 2. The list may not grow against the base branch: no new file, no higher
//    count than `origin/main` has.
// 3. The `.oxlintrc.json` override names exactly the allowlisted files.
// 4. No `eslint-disable` / `oxlint-disable` comment names `complexity`.
scripts/check-complexity.ts:9-15

The count must match exactly: one more is a new unguarded write (guard it, never raise the count); one fewer means the entry is stale (lower it, or delete it at 0). The list only ever shrinks

scripts/check-fs-writes.sh:25-27

双向校验是重点:多一处命中失败,少一处命中也失败(说明白名单条目过期了,必须下调或删除)。这同时堵住了人的惰性和 AI 的「注释掉这条规则」冲动。

25
effect-guards 放行命中
3 个类别
6
frontend-state 放行命中
2 个类别
39
background-jobs 放行命中
4 个类别
110
complexity 放行命中
100 个文件
口径:4 份 JSON 白名单合计 170 处放行命中,由本报告实际执行 node -e 统计;另有 5 处内嵌于 shell 脚本(fs-writes 9 / db-clients 10 / clip-inserts 7 / file-tokens 12 / env-example 4)。

有意区分「提交前」与「CI」

.husky/pre-commit 跑 20 项,但刻意漏掉三样,且每样都写了理由:

被留给 CI 的理由
pnpm run test(全量)「{@code @cvm/local} 的 suite 分四片全跑,因此本地定向是安全的」docs/agents/testing.md:21「The full suite: CI, not you」
check:response-body「CI ONLY … 它跑起来只要 0.2 秒,但提交 hook 是个热循环;而这个 pattern 影响的是路由里的 Response,CI 早就能看到」scripts/check-response-body.sh:21-26
oxlint 全量「Oxlint runs advisory, not as a gate … 只有把成文标准编码进去的规则才是 error,而且这批现在是绿的、必须保持绿」.oxlintrc.json:4-9

结论很清楚:快的、能立刻止血的进 hook;慢的、和提交频率无关的留给 CI。 而且 oxlint 只把 7 条规则设为 error——每条都带 // CODING STANDARD: "…" 注释指回文档章节名。

07实践五:类型纪律与测试纪律,都是对着「AI 会怎么写」下手的

7.1 「每个 any 都是一处泄漏」——一条刻意没有脚本的规则

An any switches the type checker off for every value that flows through it. Write the type you mean: …
The cost, from this repo: makeLoader took ManagedRuntime<any, any> with an unconstrained R, so a route asking for an unprovided service type-checked across all 111 route modules and the Animatic page 500ed on every load.
A new any needs a reason in the PR; a file you touch should leave review with fewer of them than it had.

CODING_STANDARDS.md:117-133
为什么这条偏偏不设脚本? 因为它是一条流程约束:「新的 any 要在 PR 里说明理由」。规则能否被机器执行,和它是否被人真正读到,是两件事。作者的做法是把估算不了的交给流程、把能量化的交给脚本——而不是让所有规则都假装是硬的。

相比之下,能机器化的就毫不含糊。例如目录结构冶 `${packages}/core` 的边界:packages/core/.dependency-cruiser.cjs 直接写了为什么这不能是「建议」:

That API runs on a machine with no finished videos directory, no Video Files directory, no ffmpeg, no OBS and no git checkout. So the rule is not "please avoid the filesystem" — it is that a filesystem import here is a build failure, caught before anything is deployed, rather than a runtime error on a box nobody is watching.

packages/core/.dependency-cruiser.cjs:3-13(FILESYSTEM_BOUND 共 23 个模块模式)

7.2 测试:两档制 + 只在系统边界 mock + 一张「不值得测」清单

本地 定向跑

pnpm --filter <pkg> test -- path/to/thing.test.ts,跑你改的文件对应的那一个测试文件。全量 suite 交给 CI。

还留了个坑的记录:仓库根目录必须用 pnpm run test:root <path>(不带 --),因为 pnpm 9.12.3 会把字面 -- 透传给 vitest,导致静默跑全量。

docs/agents/testing.md:17

CI 全量跑

test.yml 拆成 5 个并行 job(static / rest / local 四分片 / browser / client-build),最后汇成一个名为 check 的聚合 context——这是 GitHub 规则集唯一要求的状态检查。

文件顶部写着:"Do not rename it, and do not give any other job that name."

.github/workflows/test.yml:26-27

mock 策略:只在系统边界 mock

Mock at system boundaries only — external APIs, time and randomness, and the file system or a database when a real instance isn't practical. Everything inside the boundary goes in real: never mock your own classes, modules or internal collaborators. When something is hard to test without mocking an internal, redesign the interface.
Every test must be able to fail for a plausible real bug a user or caller would see; if you can't name the bug, don't write it.

CODING_STANDARDS.md:397-409

为什么这条对 AI 特别重要?因为「我该 mock 什么」对 AI 来说是一道极易产生分歧的题。docs/TESTING_STANDARDS.md:128-132 把答案收敛到了接口形态层面:

Prefer SDK-style interfaces over generic fetchers at boundaries — each function is independently mockable with a single return shape, no conditional logic in test setup.

docs/TESTING_STANDARDS.md:128-132

明确列出了「不值得写测试」的五类东西

类别说明
Tautologiesmock 返回它被要求返回的东西、复述常量/配置/查表、静态 markup 快照、「类型是个类型」
Implementation details调用次数、调用顺序、私有 helper、内部状态形状——任何「无害重构就会碎」的断言
Near-duplicates同一个接缝上换一个字面量再写一遍
Glue透传、一行映射、解析后转发的路由——去测它转发到的那个
Someone else's code「zod 能解析」「React 能渲染」「drizzle 能插入」
docs/TESTING_STANDARDS.md:18-27

还有一条用未来时态封死的绝对禁令:

Never write an automated test against a Remotion renderer package's actual render output — not for the packages that exist today, not for any added later. A real render boots Chromium, takes minutes … and asserts on pixels that a deliberate branding change is supposed to move; the test then fails for the one reason that is not a bug.

docs/TESTING_STANDARDS.md:136-140

浏览器测试的门槛被写得极低,但需要时却能回得到真机

全仓只有 9 个 *.browser.test.tsx(对比 622 个 node 测试)。规范划定只有两种情形值得写:

其余一律抽出纯函数用 node 测。测试数据库用「一个共享 Postgres 容器 + 每个测试文件一个独立库」(ADR 0046,端口 54329,数据在 tmpfs),并对 AI 最容易踩的坑做了预告:

A test that passed on PGlite and fails here is usually an order the query never promised (IN (…) or a join without ORDER BY), or a race the in-process database used to win. Fix the query or the race, not the database.

docs/agents/testing.md:32

7.3 React 两条规则,直指 AI 最爱写的模式

「从 action 里重定向,不要从 effect 里」

The useEffect pattern is fragile: if any dep (e.g. an inline onOpenChange prop) changes between renders, the effect re-fires and re-issues navigate(...), cancelling and restarting the in-flight navigation in a loop.

CODING_STANDARDS.md:316-335

「乐观 UI 从 fetcher.formData 派生」

This eliminates state-sync bugs and removes the need for useEffect entirely.
配套:每个界面一个纯 reducer,事件是事实、effect 是 reducer 返回的数据,测试直接喂事件序列而不渲染 —「No decision lives in useState + useEffect」。

CODING_STANDARDS.md:349-356

08实践六:给智能体一个「能撒野但追得到账」的真沙箱

大部分 AI 编码实践的验证环节是「跑一遍测试」。这个仓库的答案是:起一个真的应用、连一份真的(克隆的)数据库、用真的浏览器点一遍,并且把每一次写库都记进账本。

verify-cvm:417 行的技能 + 2964 行的配套脚本

机制怎么做的
数据克隆每次 run 从模板库 cvm_verify_template 克隆一份专属库 cvm_verify_<id>,允许写,cleanup 时丢弃;并自动补 apply 本 worktree 的迁徙
端口隔离验证专用 5200-5299 段;CVM 自己占 5170-5199(5173 是 Matt 的 dev server)。doctor 会断言「端口的占有者是本次 run 记录的 pid 或其直接子进程」
外部服务封印所有凭证换成废品;YOUTUBE_API_URL/BUFFER_API_URL/S3_ENDPOINT/ANTHROPIC_BASE_URL/DROPBOX_*/CLOUDINARY_* 一律指向注销端口 127.0.0.1:9,失败是朝关闭方向失败
Write Ledger在克隆库上装 statement 级触发器,把每次提交的 insert/update/delete/truncate(含 application_name)记进 verify_ledger.write;再叠加服务端 CVM_LOG_SQL=1 的语句日志
run id 是唯一句柄没有「默认 run」「latest」,没有环境变量:不带 id 的动词直接失败,不是本 worktree 起的 run 也拒绝
三条边界(脚本级别的拒绝):① 永不在录制时启动(自动 PAUSED 轮询);② 永不由智能体执行 db:verify-snapshot(唯一读生产的步骤,脚本本身会拒绝非 Matt 的 shell);③ 生产模式即使被指名也只有只读连接。
Write Ledger 是全 repo 最值得抄的一个设计 它明确拒绝三种「蒙」「蒙」:
· 不用 pg_stat_user_tables 计数——那些计数器有滞后,「曾把一次真实的 UI 保存判成 clean」;
· 三态判词:PENDING(5 秒后仍有未提交写)、UNKNOWN(表没有触发器 / 服务端没打日志)、命中就永远不许说 clean;
· 把「点了什么 → 落到哪张表 → 哪一行」变成可复Directory的账单:「一个本应写一张表的按钮写了三张表,这就是这次运行发现的一个 bug」。
.claude/skills/verify-cvm/SKILL.md:290-356

由此推出的一条硬纪律

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. … A change with no UI effect says so in one line — No UI change — instead.

CLAUDE.md:23

并且 Pr body 必须写明「你点了什么、看到了什么」,附截图或证据路径。merging.md:3 甚至要求:「记得在 worktree 被删之前把 .verify/ 里的证据目录拷出来」——因为 .verify/ 随 worktree 一起消失。

09实践七:把 LLM 输出当成一台不可靠的机器来设计接口

仓库把「让模型吐 JSON」这件事拆成了四个工程决策,每个都值得单独讲。

9.1 有副作用 → 两遍;无副作用 → 一遍

The brittle part of structured output is asking the agent to both do the work and emit rigid JSON in a single turn — it frequently returns malformed JSON or omits the <output> tag, and a single failure aborts the whole run. This wrapper splits the two concerns, which matters when the produce phase has side effects we must not repeat (commits, issue creation):

.sandcastle/run-with-extraction.ts:32-62
策略用在哪行为
runWithExtraction
两遍
review / implement-pr / update-branch / architecture-review第一遍不带 schema 干活(commit 落在这里);第二遍 resume 同一会话,用 extraction.md 让它「只吐 JSON、不许改任何东西」
runWithRetry
一遍
write-pr / write-prd-pr / to-issues-prd输出本身就是产出(写 PR 标题、拆 slice),失败则 resume 重试,最多 3 次

两遍什么东西一一对应:仓库里恰好 4 个目录有 extraction.md,也恰好 4 个脚本用 runWithExtraction。

9.2 重试不是重跑,而是 resume 同一个会话 + 精确反馈

// Retry: resume the failed session and feed back what went wrong. The
// session still holds everything the agent did, so it only re-emits.
.sandcastle/run-with-retry.ts:63-72(默认 maxAttempts = 3,由 run-with-retry.test.ts 固化为 3 次调用)

反馈文本由 retry-feedback.ts 生成,分两种情形,且明确 「Do not change any code — only the output.」:

9.3 宽进:schema 主动容忍模型的漂移

这是最容易被忽略、却最能提高成功率的一点。review-output.ts 不要求模型用对字段名:

9.4 严出:幻想锚点在编排层被丢弃

const fileLines = diffLines.get(c.path);
if (!fileLines) {
  console.warn(`Dropping inline comment for ${c.path}:${c.line} — file not in diff.`);
  return false;
}
.sandcastle/review/review.ts:190-206(diffLines 来自 parse-diff-lines.ts 对 git diff main...HEAD 的解析)

同理,回复评论时的 commentId 也要经过真实拉取到的 thread id 白名单过滤。「schema 宽进,语义严出」——接受模型说得不标准,但不接受它指着一个不存在的 md 行。

定期跑的任务如何不把 backlog 变成垃圾场 improve-codebase-architecture-project 技能定义了极严格的「已提议」判据:一条候选只要满足任一条件就算重复——① 触及与历史提案实质相同的模块;② 针对同一处摩擦,哪怕换了个角度(原文:"it addresses the same underlying friction, even with a different angle")。存疑时判为重复,理由是 "duplicates spam the backlog"。
更进一步:被关闭且留下实质性理由的历史提案,其理由对新提案具有约束力,不得重新提出符合该理由的方案。
实现方式是强制先读取全部历史提案(gh issue list --label source:architecture-review --limit 200)——这是让「每天 cron 上跑 LLM 产出提案」不至于熵增成垃圾场的唯一办法。 .claude/skills/improve-codebase-architecture-project/SKILL.md:31-76

10实践八:把「拒绝」设计成默认动作,而不是失败后的补丁

八个 workflow 的共同结构是:Preconditions & refusals 排在 Step sequence 前面。也就是说「我凭什么不该跑」是第一步,不是异常分支。

拒绝情形行为
已有 open PR 引用了 Closes #N拒绝,不进入 agent:in-progress,comment 说明原因
嵌套 PRD(PRD 自己是子 issue,或子 issue 又有子 issue)拒绝 + agent:blocked
agent:queued 被贴到子 issue 上移除 queued、加 blocked、comment 说明「queued 对子 issue 无意义」
还有别的阻塞没关静默跳过(GitHub UI 已经显示了剩余阻塞),不留 comment
push 被拒绝且判定为租约冲突写入 failure_reason.txt「Branch advanced during review run.」,race=true
任何失败failure() 步加 agent:blocked + comment;always() 步移除 agent:in-progress

最后一条很关键:agent:in-progress 既是状态标签也是分布式锁——「加锁是『接受』的一部分,解锁必须是 always()」,否则一次崩溃就把这个 issue 永久焊死了。

失败信息必须能被拿到

失败处理的第一步是读 ${RUNNER_TEMP}/failure_reason.txt,缺失时不是假装没事,而是回落到字面说明:

"(no reason file written — check workflow logs)"
afk-agent-platform-spec.md §3.7

恢复路径也写得毫不含糊——comment 永远以这句结尾:Re-add \`agent:implement\` to retry.。失败要给出一条明确的人工恢复动作,而不是一句「something went wrong」。

11实践九:56 篇 ADR,把「为什么」变成可检索的第一类资产

docs/adr/ 下 56 篇,编号到 0053。它们不只是给人类看的架构记录,更是给下一次智能体运行准备的「抗重复护栏」。

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

docs/agents/domain.md:47-51

ADR 的命名本身就在说人话,例如 0026-migrations-applied-by-hand.md、「0038-recording-is-gated-on-an-av-calibration.md」、「0041-storage-moves-keep-originals-and-never-delete-footage.md」——文件名就是决策。

典型示范:ADR 0022 如何精确划定人机边界

作者要给智能体开 Deliverables 日历的写权限,但 ADR 0007 说过这个日历是「manual」。于是他没有绕过,也没有含糊,而是把这个词拆成两个意思:

含义要不要保理由
manual = not derived(非派生)保留条目可以先于它所描述的实体存在、可以被推测性地挪动 —— 这是 0007 真正的决定
manual = hand-typed by a human放弃「an incidental fact about the only write surface that existed when 0007 was written」

最后甚至考虑并否决了「不写 ADR」这个选项,理由写得极好:

Treat this as a plain implementation detail and write no ADR. Tempting — no decision in ADR-0007 is being reversed. Rejected because a future reader who finds an agent writing the "manual" calendar would reasonably think 0007 had been violated; the cheap fix is to say once, here, that it hasn't.

docs/adr/0022-deliverables-are-agent-writable.md · Considered alternatives
顺带解决了「作用域蠕变」 这篇 ADR 还明确划出了本决策不覆盖的东西:如果将来需要让智能体提议而不是设定日期,那是一个全新的决策(需要在 Deliverable 上新增「提议态」),不在本 ADR 范围内。
同样被写死的还有:「这里完全没有增加任何方向的传播」——设置 --status done 不会去改任何关联 Pitch 的存储状态(ADR-0009 已经删掉了它),它只改变派生出来的 Pitch State,与 UI 里人工点击的行为完全一致。

12量化复盘

100% 50% 0 2507 2508 2509 2510 2511 2512 2601 2602 2603 58% 2604 56% 2605 35% 2606 53% 2607 44% 2608 2609 2610 22% 86% 89% 85% 77% 44% 柱高 = 该月带 Co-Authored-By: Claude 的提交占比;2026-10 若只看非合并提交则为 100%(573/573) 口径:git log --no-merges + %(trailers:key=Co-Authored-By,valueonly)
图 4 16 个月的 AI 协作渗透曲线。2025-12 之前基本为 0;2026-01 至 03 维持 85%–89%,此后在 35%–77% 之间波动——人工休假或出差时曲线就塌下去。

AI 原生能力的引入节奏

日期文件意义
2025-10-21CLAUDE.md第一份智能体契约文件(引入当天就有 4 个 Claude 署名提交)
2026-03-25.sandcastle/main.ts本地版 Plan→Implement→Review→Merge 编排器(10 轮 × 4 并发)
2026-03-26.github/workflows/agent-implement.yml
docs/agents/afk-agent-platform-spec.md
把编排搬到 GitHub Actions,并同步写下 1092 行规范
2026-09-26CODING_STANDARDS.md把散落的标准收敛成一份「写代码时读」的总纲
2026-10-07GLOSSARY.md领域语言定稿——有了它,智能体才有了统一的词表

注意这个顺序:先有 workflow(能跑)→ 再有规范文档(能复现)→ 再有编码总纲(质量)→ 最后有术语表(语义一致)。不是先写文档再照着做,而是跑起来之后把做对了的部分固化成文档。

模型迭代史:一次可以直读其协作革命的时间线

提交 trailer 里留下了 16 种不同模型的署名,等于一份免费的「作者换模型史」:

Co-Authored-By 署名分布(前 8,共 2200 份 trailer) Claude Opus 4.6 · 858 Claude Opus 5.5 · 614 Opus 4.5 · 238 Opus 5 (1M) · 107 Sonnet 4.5 · 95 Opus 4.8 (1M) · 85 Sonnet 4.6 · 60 Opus 4.7 (1M) · 40 注:这只是 trailer 中的模型名,不等于实际执行的全部模型; 但仍可作为「半年内跨代模型在同一个仓库共存协作」的证据。 同一套约定文件要同时对 16 种模型版本生效, 这本身是对「约定是否写得够清楚」的长期压力测试。
图 5 模型署名分布。条形宽度按 max 858 归一化。

一次性看的规模表

指标数值口径
应用代码行数341,650find apps packages -name "*.ts*" 排除 node_modules/dist
非 git 文件数2,506find . -type f -not -path './.git/*'
node 测试 / 浏览器测试622 / 9浏览器测试刻意保持极低
ADR56docs/adr/,编号至 0053
Claude 技能9.claude/skills/,合计 1,030 行 SKILL.md
verify-cvm 技能包2,964 行1 个 SKILL.md + 9 个 shell 脚本 + 8 份 feature/验收文档
提示词文件26.sandcastle/ + docs/agents/prompts/
静态守卫1913 个 shell + 6 个 TS
CI 并行 job5 → 1汇聚成单一必需的 check context

13代价、文档漂移与风险|这个体系里已经出现裂缝的地方

复盘如果不写代价就是广告。以下是实际在仓库里能核对出的问题。

风险 1

文档与代码已经漂移(至少 3 处)

  • ADR 0008 落后于代码:它的第 5 行说单 issue 流程「follows the same rule by omission: it has no commit check」,但 .sandcastle/implement/implement.ts:29-34 里有 git rev-list --count main..HEAD == 0 → fail。规范 §4.2 已经跟上了,ADR 没有。
  • 模型名漂移:附录 A 写 claudeCode("claude-opus-4-6"),8 个脚本里实际全是 claude-opus-5。
  • CI 复用缺失:8 个 agent workflow 各自内联重复了 .github/actions/setup 的三步。

对策:把这三处做成 CI 里的断言脚本(就像 check-effect-tsc.ts 对 tsc 补丁做的那样)——「文档是否还能描述现实」本身也应该是机器可验证的。

风险 2

自证式测试:AI 写实现,也写「证明它对」的测试

规范写得再严,TESTING_STANDARDS.md 拦不住「一个只可能由于当前实现才会失败的测试」。这正是 verify-cvm 存在的理由——它是一条不经过被测代码作者的路:真浏览器、真数据库、账单式的 Write Ledger。

已经做到的:把「UI 有可见改动必须在浏览器验证」写进 CLAUDE.md 硬约束 + PR 模板要求。
还没做到的:这条约束目前依赖诚实 reporting——No UI change 是一行自我声明,没有机器校验。

风险 3

白名单可能变成「合法性外壳」

170 处放行命中 + 至少 42 处 shell 内嵌条目,合计超过 200 个例外。虽然「只减不增」堵住了增长,但它不能阻止人们在一个已有条目范围内继续堆代码(例如 complexity 白名单某个文件的计数上限是某个数字,在那个范围内还可以加)。

对策:给白名单条目加过期日期,或让 CI 定期生成「最老的 10 条债务」报告。

风险 4

人类审批是吞吐瓶颈,而且是单点

全流程唯一的 merge 出口是 Matt 本人;连快照生产数据(db:verify-snapshot)也只有他能跑。这对一个单人项目是合理的,但它意味着这套系统的吞吐上限被一个人的注意力钉死——2026-04、2026-06 的 AI 占比跌到 58%/35%,很可能就是这种「人不在」的直接结果。

对策(已在路上):agent:queued + 原生依赖关系的自动晋升,正是为了减少「等人想起来」的损耗。

最需要诚实对待的一条 这套体系的可靠性来自「人类仍然审查每一个 merge」。它不是「AI 全自动写代码」,而是「AI 全自动准备一个人类只需要花 3 分钟就能有信心批准的东西」。
如果你打算把这套搬到自己的项目上、并顺手拿掉人的审批环节,那么前面所有的机制——1092 行规范、19 个静态守卫、Write Ledger、强制的浏览器验证——都会失去意义。它们存在的价值是降低审批成本,而不是消除审批。

14可复用最佳实践清单

按「投入产出比」从高到低排序。前 5 条一周内可以落地,后 5 条需要投入工程时间。

#实践怎么做成本证据
1 单一智能体契约入口 只维护一份 CLAUDE.md(或 AGENTS.md),不要同时存在多份工具专属约定文件。它只放「去读哪」+「绝对不能做什么」,正文全部下推到 docs/ 极低 CLAUDE.md(81 行)
2 区分 give-me 与 read-me 的文档 给人看的 README 和给智能体看的 docs/agents/ 分开。操作规程像写 API 文档一样写:命令、分支命名、失败恢复动作都要可执行 极低 docs/agents/{merging,testing,backlog}.md
3 把 AI 常踩的坑写成一句话禁令 把 AI 最常撒野的点写成一句话禁令,配上真事故:「新的 any 要在 PR 里写理由」「不许在 ↑ useEffect 里做重定向」 低 CODING_STANDARDS.md:117-133, 316-335
4 先有领域词表,再写代码 一份术语表 + 「要避开的同义词」列。 pit进一步要求:智能体输出(issue 标题、重构提案、测试名)必须用术语表的叫法 低 GLOSSARY.md · docs/agents/domain.md:41-45
5 列出「不值得做的事」清单 不满足系统边界的「哪些东西不许 mock」、五类「不值得写测试」的东西、以及一条针对性的绝对禁令。AI 的毛病通常不是不会做,而是做得太多 低 docs/TESTING_STANDARDS.md:18-27
6 为每一次「类型失效」写一个守卫脚本 凡是有 Leak(as any、反射、字符串拼装)绕过类型系统的地方,配一个 scripts/check-*.sh,并在注释里写明「庄名哪一个 bug」 中 scripts/check-response-body.sh:14-15
7 技术债用「只减不增」白名单编码 引入新规则时把历史债记进 allowlist;守卫要双向校验:多一处失败,少一处也失败(条目过期需下调)。且禁止用 disable 注释绕过这条规则 中 scripts/{effect-guards,complexity,fs-writes}*
8 编排器与执行器之间只通过「文件」通信 env 变量 + 上下文进,产出文件出。编排器(CI)负责所有远端变更,执行器(智能体)不持 token。这让 runner 可替换、可单测 中 afk-agent-platform-spec.md:27-30, §3.8
9 结构化输出:两遍 + 宽进严出 + resume 重试 有副作用(commit/comment)→ 干活一遍不带 schema,提取一遍带 schema;schema 容忍别名与类型转换;语义层面的幻想(不存在的行号/评论 id)由编排层按真实 diff 丢弃;重试是 resume 会话而非重跑 中 .sandcastle/{run-with-extraction,run-with-retry,retry-feedback}.ts
10 把决策写成 ADR,并写出「本决策不覆盖什么」 每个 ADR 必须答两件事:被否决的替代方案为什么被否,以及本决策不覆盖什么。后者比前者更能防止后来的误读——一个未来的读者(包括 LLM)能据此判断边界在哪 中 docs/adr/0022 · 0008
11 拒绝优先于重试 把「我凭什么不该跑」写成第一步;拒绝时不进入进行中状态。锁标签的加锁是「接受」的一部分,解锁必须是 always()。失败 comment 永远以「重新贴某标签即可重试」结尾 较高 afk-agent-platform-spec.md §3.7, §3.9
12 给智能体一个「能撒野但追得到账」的沙箱 真应用 + 数据克隆 + 允许写 + Write Ledger 记账 + 外部服务全部指向注销端口 + 「不得低于 clean 判断」。把「UI 有可见改动必须在浏览器验证」写进 PR 硬要求 高 .claude/skills/verify-cvm/
如果只抄一条 抄 第 7 条——「只减不增的白名单」。它是唯一能用一个文件同时堵住「人的惰性」「AI 的绕过冲动」和「技术债只增不减」三件事的机制,而且不需要任何 LLM 基础设施:一个 JSON、一个 check 脚本、一条 CI 规则就够了。

15证据索引

按本报告论述顺序排列。所有路径相对于仓库根目录。

路径体量支撑的结论
CLAUDE.md81 行唯一的智能体契约;指针式组织;浏览器验证硬要求;cvm CLI 的单一传输约束
CODING_STANDARDS.md410 行every any is a leak(附 111 个路由模块事故);React 两条规则;背景作业;援助手段
GLOSSARY.md578 行 / 134 条领域语言 + 要避开的同义词
docs/agents/afk-agent-platform-spec.md1092 行四大支柱;§3.5 并发;§3.6 push;§3.7 失败;§3.8 运行器契约;§3.9 不变量
docs/agents/merging.md23 行五步落地法;禁止 --no-verify;管理员专属ॉ操作
docs/agents/testing.md36 行本地定向 / CI 全量;反 diff 启发式;ADR 0046 测试库;PGlite 迁移坑
docs/agents/domain.md51 行先读 GLOSSARY + ADR;缺失则静默继续;ADR 冲突要明说
docs/agents/triage-labels.md28 行规范标签 ↔ 真实标签映射;agent:* 状态标签
docs/agents/queued-promotion.md40 行依赖解除后的自动晋升;幂等翻牌;AGENT_PAT 与降级
docs/agents/prompts/*.prompt.md8 份CI 注入智能体的提示词模板
docs/TESTING_STANDARDS.md140 行边界 mock;五类不值得测;浏览器测试门槛;Remotion 禁令
docs/EFFECT_STANDARDS.md96 行CODING_STANDARDS 的推理层 + 强制手段
docs/adr/0008-agent-implement-zero-commit-runs.md~22 行零提交 = 成功的改判 + 实证事故编号
docs/adr/0022-deliverables-are-agent-writable.md—「manual」→「underived」的边界重划;考虑并否决的三种方案
docs/adr/0026-migrations-applied-by-hand.md—生产迁移只由人执行;智能体永不做
docs/adr/0046-tests-share-one-postgres-container-*.md—一容器一测试库;ADR 0046(与 0043 同号,注意编号复用)
.github/workflows/agent-*.yml8 份触发、并发、权限、push 策略、失败处理
.github/workflows/test.yml170 行5 个并行 job 汇聚成单一 check context;文档快路径
.sandcastle/main.ts174 行本地编排器:10 轮 × 4 并发的 Plan/Implement/Review/Merge
.sandcastle/run-with-extraction.ts97 行两遍提取的设计理由(有副作用时不可重跑)
.sandcastle/run-with-retry.ts100 行resume 重试,最多 3 次
.sandcastle/retry-feedback.ts82 行把 schema 校验错误渲染成人话反馈
.sandcastle/review/review-output.ts—宽进:path/file、body/comment、line/lineRange 别名
.sandcastle/review/review.ts:190-206—严出:不在 diff 里的行号评论被丢弃
.sandcastle/no-recent-commits.test.ts27 行用测试冻结 prompt 反模式(禁止 <recent-commits>)
.sandcastle/implement/prompt.md32 行TDD 三段;不得自行关 issue;不得用 RALPH 前缀
.claude/settings.json15 行PreToolUse 钩子:拦 npx tsc,引导到 pnpm run typecheck
.claude/hooks/block-npx-tsc.sh14 行仅在 npx 是首个词时拦,避免误伤提交信息
.claude/skills/verify-cvm/SKILL.md417 行数据克隆 / 端口隔离 / 外部服务封印 / Write Ledger / 录制期间暂停
.claude/skills/do-work/SKILL.md29 行本地四阶段:探索→实现→反馈环→提交
.claude/skills/improve-codebase-architecture-project/SKILL.md132 行cron 架构评审;删除测试;防止重复提案
.claude/skills/to-prd-project/SKILL.md86 行PRD 模板;「别写具体路径和代码,它们会过期」
scripts/check-*(19 个)—机器守卫总目录
scripts/*-allowlist.json(4 份)170 命中只减不增的技术债账本
packages/core/.dependency-cruiser.cjs—core-is-filesystem-free;「不是请避免而是构建失败」
packages/core/db/connection-guard.ts~145 行worktree 内禁止可写远程连接
.husky/pre-commit20 行提交前的 20 项(刻意不含三项)
.oxlintrc.json314 行advisory 而非门禁;只有 7 条 error 且指回文档章节
package.json:59-61—check:static / check 的完整定义
plans/ralph.sh · plans/backlog/{once,afk,setup,cleanup}.sh—本地 Docker sandbox 版自治循环
plans/backlog/prompt.md52 行tracer bullet 优先级;RALPH 提交信息五要素;一次只做一个任务
docs/plans/test-pruning.md:238—把「约定守卫测试」列为「keep · high」