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.
本地还有另一条线: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
❌
—
✅ Matt
ADR 0026 · README.md:68
跑 db:verify-snapshot(唯一读生产的快照)
❌ 脚本拒绝
—
✅ Matt
verify-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)承认并写死的行为,不是意外。
// 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 的「注释掉这条规则」冲动。
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 里说明理由」。规则能否被机器执行,和它是否被人真正读到,是两件事。作者的做法是把估算不了的交给流程、把能量化的交给脚本——而不是让所有规则都假装是硬的。
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.
文件顶部写着:"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
明确列出了「不值得写测试」的五类东西
类别
说明
Tautologies
mock 返回它被要求返回的东西、复述常量/配置/查表、静态 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.
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 编码实践的验证环节是「跑一遍测试」。这个仓库的答案是:起一个真的应用、连一份真的(克隆的)数据库、用真的浏览器点一遍,并且把每一次写库都记进账本。
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.
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):
const fileLines = diffLines.get(c.path);
if (!fileLines) {
console.warn(`Dropping inline comment for ${c.path}:${c.line} — file not in diff.`);
return false;
}
同理,回复评论时的 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
If your output contradicts an existing ADR, surface it explicitly rather than silently overriding: _Contradicts ADR-0007 (event-sourced orders) — but worth reopening because…_
「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 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 补丁做的那样)——「文档是否还能描述现实」本身也应该是机器可验证的。