乘风行远

专注于后端、Vibe Coding邻域开发

0%

grill-with-docs 与 Trellis 协同编码实践

grill-with-docs 与 Trellis 协同编码实践

副标题:从审问到可运行代码——以「API 速率限制中间件」为例的全流程新手实操(Claude Code + Codex 双平台)

你将完成什么

跟着这篇做一遍,你会得到一个 可运行的功能:一个挂在 /api/* 上的速率限制中间件,已认证用户按 user.id 限流、未认证按 IP,令牌桶算法,超额返回 429。更重要的是,你会完整走一遍两个工具怎么配合:

  1. grill-with-docs 在动手前把”按什么限、用什么算法、计数器存哪、超额返回什么”一个个审清楚,审的同时自动产出三份文档。
  2. Trellis 把这三份文档在代码生成的那一刻交给写代码的 agent,并用质量门把守”没过 lint/type/test 不准交”。

两个平台都覆盖:Claude Code 全程自动化最完整;Codex 同样能走完全程,只是其中两步(规范注入、质量门)从”自动”变成”手动”。文章对每一步都给出两个平台的做法。

命令记法:凡是 Trellis 命令,/xxx 表示 Claude Code,$xxx 表示 Codex(如 /start$start),两平台命令一一对应。真正有差异的只有 Step 4(规范注入)Step 5(质量门),那两步会单独分栏说明。

前提 :一台装了 Claude CodeCodex CLI 的机器,一个已有的小后端项目(Node/Express 之类,有几个接口)。还没有的话,照 附录 A花一分钟搭一个最小的。

文中的对话样例和 agent 输出是为讲解而构造的示意(不是某次真实运行的逐字记录),但每一步的命令、文件内容、最终代码都是可以直接照抄运行的。


1. 两个工具分别干什么(30 秒)

grill-with-docs Trellis
干什么 动手前 逐题审问 需求,边审边产出术语表和决策文档 把项目规范在代码生成的那一刻交给写代码的 agent,并自动把关质量
产物 CONTEXT.md(术语表)、docs/adr/(架构决策)、编码约定 代码 + 通过 lint/type/test 的质量门
解决的痛点 “想清楚再写” “写下来的标准,AI 真的会读”

它们天然互补:grill-with-docs 出料 (决策和文档),Trellis 送料(把文档送到代码生成的那一刻)。单独用 grill-with-docs,审出来的决策写完就躺在仓库里,下次写代码没人保证它被读;单独用 Trellis,注入机制很强,但 .trellis/spec/trellis init 后是空的,送的是空气。

先把 grill-with-docs 的全部内容贴出来——它只有五行:

1
2
3
4
5
6
7
---
name: grill-with-docs
description: A relentless interview to sharpen a plan or design, which also creates docs (ADR's and glossary) as we go.
disable-model-invocation: true
---

Run a `/grilling` session, using the `/domain-modeling` skill.

一个壳,真正干活的是另外两个 skill:grilling(审问)和 domain-modeling(写文档)。所以装的时候要三个文件一起装,内容见 附录 B

平台能力对照(来自 Trellis 官方文档第 13 章):

能力 Claude Code Codex 差异说明
会话上下文注入 ✅ 自动 ✅ 自动 都有 session-start.py,开会话即注入身份 / 历史 / 任务
把 spec 注入写代码的 agent ✅ 自动 ❌ 手动 Codex 无 PreToolUse Hook,Step 4 改手动点名
质量门(lint/type/test 不过打回) ✅ 自动 ❌ 手动 Codex 无 Ralph Loop,Step 5 改手动跑验证
多 agent 自动流水线 ✅ 自动 ⚡ 半自动 Codex 有 .codex/agents/*.toml,但需手动串步骤
命令前缀 / $ /start$start
Skill 目录 .claude/skills/ .agents/skills/ 初始化时分别生成
命令数量 13 13 一一对应

一句话:两个平台都能跑完全程,区别只在 Step 4、Step 5 谁来按”读规范”和”跑测试”这两个按钮——Claude Code 是机器按,Codex 是你按。


2. 环境准备(Step 0)

装 Trellis 并初始化。按你的平台二选一

1
2
3
4
5
6
7
8
npm install -g @mindfoldhq/trellis@latest
cd your-project

# Claude Code(init 自动检测平台)
trellis init -u 你的名字

# Codex(显式指定 --codex)
trellis init --codex -u 你的名字

-u 你的名字 会成为你的开发者身份,并创建个人工作区 .trellis/workspace/ 你的名字 /。两边都会生成 .trellis/(spec / workspace / tasks / scripts)。区别在配置目录:

平台 生成的配置 关键文件
Claude Code .claude/ commands/(13 个命令)、agents/(6 个 agent)、hooks/3 个 hook
Codex AGENTS.md + .agents/ + .codex/ .agents/skills/.codex/agents/*.toml.codex/hooks/session-start.py

验证装好了——同样按平台:

1
2
3
4
5
6
7
8
9
10
11
# Claude Code:检查三个 hook(这是它的发动机)
ls .claude/hooks/
# 应看到:session-start.py inject-subagent-context.py ralph-loop.py

# Codex:检查入口和会话 hook
ls AGENTS.md .codex/hooks/session-start.py .agents/skills/
# AGENTS.md 是 Codex 入口(类似 CLAUDE.md),session-start.py 注入会话上下文

# 两个平台都检查 spec 占位
ls .trellis/spec/backend/
# 应看到一堆 .md,里面写着 "(To be filled by the team)"

Claude Code 那三个 hook 各管一段:

  • session-start.py —— 每次开会话自动注入上下文(你是谁、上次干到哪、有哪些任务)。
  • inject-subagent-context.py —— 主 agent 调子 agent 时拦截,按 JSONL 把 spec 塞进子 agent 的 prompt。Codex 没有它,所以 Step 4 要手动替代。
  • ralph-loop.py —— check agent 说”做完了”时拦截,跑验证命令,没过就打回去修,最多五轮。Codex 也没有它,所以 Step 5 要手动替代。

Codex 有自己的 session-start.py(会话上下文照常自动注入),所以开会话、接上历史这两件事,两边体验一致。

可选:跑一次 /start(Codex 是 $start),AI 会读 workflow、读 spec 索引、报上下文,然后问你 “What would you like to work on?”。两边因都有 session hook,这条命令其实可选,但第一次建议跑一遍看清流程。


3. 装上 grill-with-docs(Step 1)

grill-with-docs 是个壳,需要连 grillingdomain-modeling 三个 skill 文件一起放进 你平台的 skill 目录 。完整内容在 附录 B,目录结构:

1
2
3
4
5
6
7
8
9
10
11
# Claude Code
.claude/skills/
├── grill-with-docs/SKILL.md ← 壳:触发审问 + 调用下面两个
├── grilling/SKILL.md ← 审问规则:逐题、给推荐答案、只问决策
└── domain-modeling/SKILL.md ← 写文档:术语表 + ADR

# Codex(同样的三个文件,放 .agents/skills/)
.agents/skills/
├── grill-with-docs/SKILL.md
├── grilling/SKILL.md
└── domain-modeling/SKILL.md

把附录 B 的三段内容分别存成这三个文件即可。

disable-model-invocation: true 的意思是它不会自己跳出来——你必须主动触发。触发方式(两平台都适用):

  • Claude Code:直接打 /grill-with-docs
  • Codex:直接用自然语言触发——“用 grill-with-docs 审一下:给现有 API 加速率限制”。

官方捷径(两平台都有):用 Trellis 自带的 /integrate-skill(Codex 是 $integrate-skill)一键集成,它会自动在 spec/guides/ 写集成文档、生成包装命令,给你统一入口 /use-grill-with-docs(Codex 是 $use-grill-with-docs)。手动建文件的好处是你清楚里面是什么,下面的步骤就照手动的来。


4. 用 grill-with-docs 审问需求(Step 2)——核心

进你的 AI 编程工具,触发 grill-with-docs,把需求丢给它:

1
你:用 grill-with-docs 审一下:给现有 API 加速率限制。

这一步 两个平台完全一样——审问靠的是 skill 里的文字规则,Claude Code 和 Codex 读到的指令相同,行为一致。

grilling 的四条规则决定了接下来这场对话的形状:

  1. 沿决策树 一题一题问,不一次抛一堆;
  2. 每题 先给推荐答案,你来确认或推翻;
  3. 能用环境查到的事实别问人——交给 research 子 agent 去翻代码;
  4. 达成共识前不动手写代码。

下面是一场完整审问的样子(示意)。AI 一次只问一题,你回答后它才问下一题;同时 domain-modeling 在后台把结论写进文件。


Q1:限流的维度——按什么限?

AI(推荐):已认证请求按 req.user.id,未认证按 req.ip。理由:按用户更精准,IP 兜底匿名流量。
你:同意。

Q2:用什么限流算法?

AI(推荐):令牌桶(Token Bucket)。允许短时突发(桶里有令牌就能消费),符合人类交互节奏。备选固定窗口 / 滑动窗口不允许突发。
你:同意,令牌桶。
→ domain-modeling 把 Token Bucket / Sliding Window / Burst 三个术语写进 CONTEXT.md

Q3:计数器(令牌桶)存哪? ← 这题值得 ADR

AI(先调 research 子 agent 查事实):查了一下,当前是单实例部署,没有 Redis。基于这个事实,推荐 进程内 Map,但把存储抽象成可替换接口(get/set),以后迁 Redis 只换实现。
AI(再问决策):你接受”现在用进程内 Map、留好 Redis 迁移口子”吗?注意多实例部署会失效。
你:接受,单实例够用。
→ 这条同时满足 ADR 三条件(难推翻 + 没上下文会困惑 + 真权衡),domain-modeling 写 docs/adr/0001-in-memory-rate-limiter-counter-store.md

Q4:窗口大小和配额?

AI(推荐):容量 60、每秒补 1 个令牌(即每分钟 60 次上限,允许一次性突发 60 次)。
你:同意。
→ 写进 .trellis/spec/backend/rate-limiting.md

Q5:超额返回什么?

AI(推荐):状态码 429,响应体 { "error": "rate_limit_exceeded", "retryAfter": < 秒 > },headers 带 Retry-After + X-RateLimit-Limit + X-RateLimit-Remaining
你:同意。
→ 写进 .trellis/spec/backend/rate-limiting.md

Q6:中间件挂在哪?哪些路由豁免?

AI(查事实后推荐):挂在 /api 子路由上;/health 直接挂在 app 上、不经过 /api,天然豁免。豁免在路由层声明,不在中间件内部硬编码。
你:同意。

Q7:怎么测?

AI(推荐):单元测试令牌桶算法;集成测试覆盖”连续 60 次后第 61 次 429 带 Retry-After”和”健康检查连续请求不限”。
你:同意。


审完,domain-modeling 已经产出三份文件。下面是它们的完整内容,你可以直接核对 / 照抄。

4.1 CONTEXT.md(术语表——只有术语,零实现细节)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Domain Glossary

## Rate Limit
限制某个主体(用户 /IP)在单位时间内能发起的请求数量的机制。关注 " 多少次 ",与 Throttle 不同。

## Throttle
在请求过快时主动减速或拒绝的处理动作。Rate Limit 是规则,Throttle 是执行规则时的行为。

## Token Bucket
一种限流算法。桶有固定容量(capacity),以固定速率补充令牌;每次请求消耗一个令牌;桶空则拒绝。允许短时突发——只要桶里有令牌就能消费。

## Sliding Window
另一种限流算法。在 " 过去 N 秒 " 这个滑动窗口内累计请求数,超阈值则拒绝。不像令牌桶那样允许突发。

## Quota / 配额
分配给某个主体的请求预算。本项目指每分钟 60 次。

## Burst / 突发
短时间内消耗超过平均速率的请求量。令牌桶允许突发(上限为桶容量),固定窗口不允许。

注意:这里 没有 任何”用 Map 存储””返回 429”——那些是实现规约,归 spec。domain-modeling 的纪律就是 CONTEXT.md 只装业务词汇。

4.2 docs/adr/0001-in-memory-rate-limiter-counter-store.md

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# ADR-0001: 速率限制计数器使用进程内存储 

- 状态:Accepted
- 日期:2026-08-10

## 背景
速率限制需要为每个主体维护一个计数器(令牌桶)。可选:进程内 Map,或外部存储(Redis)。

## 决策
当前阶段使用进程内 Map。计数器抽象为可替换接口,便于后续替换为 Redis。

## 理由
- 当前单实例部署,进程内存储零依赖、零延迟。
- 引入 Redis 增加运维负担和故障面,单实例下无收益。

## 后果
- 多实例部署时失效:每个实例各自计数,实际限额 = 实例数 × 配额。
- 重启丢失计数,少量请求可能短暂超额,业务可接受。

## 迁移信号
部署第二个实例时强制迁移到 Redis;届时只需实现接口的 Redis 版本,代价可控。

4.3 .trellis/spec/backend/rate-limiting.md(编码规范——这才是会送到代码生成那一刻的东西)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 速率限制规范 

## 算法
令牌桶(Token Bucket)。默认:capacity = 60,refillPerSecond = 1。

## 限流维度
- 已认证:按 req.user.id
- 未认证:按 req.ip
- keyFn 必须可替换(测试、灰度需要)

## 豁免路由
健康检查等路由豁免。豁免在路由层声明,不在中间件内部硬编码。

## 超额响应
状态码 429,响应体 { "error": "rate_limit_exceeded", "retryAfter": < 秒 > }。
Headers:Retry-After、X-RateLimit-Limit、X-RateLimit-Remaining。

## 文件结构
- src/middleware/rateLimit.js — 中间件 + TokenBucket + 计数器存储
- 计数器存储导出为可替换接口,当前为进程内 Map(见 ADR-0001)

## 禁止
- 禁止在限流逻辑里抛错,必须返回 429 响应。
- 禁止把存储硬编码进中间件、不给替换入口(会堵死 Redis 迁移路径)。

审问到这一步,”想清楚”的部分已经完成,而且每一份结论都落在了 它该去的地方:术语进 glossary,架构决策进 ADR,编码约定进 spec。


5. 把审问结果变成 Trellis 任务(Step 3)

现在让 Trellis 把这个需求变成一个 任务 ,并告诉它”写代码时读哪些文件”。这一步 两平台产物相同(任务目录结构一模一样)。

最省事的方式是直接对 AI 说:”按这个需求创建任务并开发”。AI 会走 Trellis 的 Task Workflow:调 research agent 扫代码 → 建任务目录 → 写 task.json / prd.md / implement.jsonl 等。但我们想确保 grill 产出的三份文件被挂进去,所以这里看清楚 implement.jsonl

任务目录长这样(名字是 月 - 日 - 任务名):

1
2
3
4
5
6
.trellis/tasks/08-10-rate-limiting/
├── task.json # 任务元数据(状态、phase)
├── prd.md # 需求文档
├── info.md # 技术设计(可选)
├── implement.jsonl # ← implement agent 要读的文件清单
└── check.jsonl # ← check agent 要读的文件清单

5.1 prd.md

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Rate Limiting

## 背景
现有 /api/* 接口无限流,单个恶意客户端可打满后端资源。

## 需求
为 /api/* 添加速率限制,超额返回 429。/health 豁免。

## 验收标准
- 已认证按 user.id,未认证按 IP。
- 令牌桶,容量 60,每秒补 1。
- 超额返回 429 + {error, retryAfter} + Retry-After / X-RateLimit-* headers。
- 计数器存储可替换(默认进程内 Map)。
- 单元测试令牌桶;集成测试覆盖 429 与豁免。

5.2 info.md

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 技术设计 — Rate Limiting

## 模块
src/middleware/rateLimit.js
- TokenBucket 类:consume / timeUntilAvailable
- rateLimit({capacity, refillPerSecond, keyFn}) 工厂,返回 express 中间件
- 计数器存储:进程内 Map(ADR-0001),导出以便替换

## 挂载
- /api 子路由启用 rateLimit
- /health 直接挂在 app,不经过 /api,天然豁免

## 测试
test/rateLimit.test.js
- 健康检查连续请求不限
- 连续 60 次后第 61 次 429,带 Retry-After 和 error body

5.3 implement.jsonl(最关键——它决定 implement agent 读到什么)

1
2
3
4
5
6
7
{"file": ".trellis/workflow.md", "reason": "Project workflow and conventions"}
{"file": "CONTEXT.md", "reason": "Domain glossary — Rate Limit / Token Bucket / Throttle 等术语的权威定义 "}
{"file": "docs/adr/0001-in-memory-rate-limiter-counter-store.md", "reason": " 进程内计数器 + 可替换接口的决策 "}
{"file": ".trellis/spec/backend/index.md", "reason": "Backend development guide"}
{"file": ".trellis/spec/backend/rate-limiting.md", "reason": " 算法、限流维度、429 响应格式、文件结构、禁止项 "}
{"file": ".trellis/spec/backend/quality-guidelines.md", "reason": " 代码质量要求 "}
{"file": "src/middleware/", "type": "directory", "reason": " 现有中间件写法 "}

注意第 2、3、5 行——这正是 grill 产出的三份文件。这一步是两个工具真正接上的地方

  • Claude Code:只要它们出现在 implement.jsonl 里,inject-subagent-context.py 会在 implement agent 跑的那一刻把它们拼进 prompt——自动、无感。
  • Codex:没有那个 Hook,implement.jsonl 不会被自动拼进 prompt。但它的价值不变:它是 implement agent 的 必读清单 。在 Step 4,你要照着这份清单 手动点名 让 Codex 读这三份文件。

5.4 check.jsonl

1
2
3
{"file": ".trellis/spec/backend/rate-limiting.md", "reason": " 核对算法、限流维度、429 响应格式、豁免 "}
{"file": "CONTEXT.md", "reason": " 核对代码里的术语是否符合 glossary 定义 "}
{"file": ".trellis/spec/backend/quality-guidelines.md", "reason": "Lint / typecheck / 禁止项 "}

激活任务(设置 .trellis/.current-task 指针,两平台通用):

1
python .trellis/scripts/task.py start .trellis/tasks/08-10-rate-limiting

6. 让 Trellis 写代码(Step 4)

这一步两个平台 做法不同,产物相同——最终代码一字不差,区别只在”那三份文档怎么进到 agent 眼前”。

Claude Code:自动注入

对 AI 说:”开始开发这个任务”。Trellis 调 implement agent,inject-subagent-context.py 拦截这次调用,按 implement.jsonl 拼出新 prompt。implement agent 实际收到的开头长这样:

1
2
3
4
5
6
7
8
9
10
11
12
# Implement Agent Task
## Your Context
=== CONTEXT.md ===
(完整术语表:Rate Limit / Token Bucket / ...)
=== docs/adr/0001-in-memory-rate-limiter-counter-store.md ===
(完整 ADR:进程内 Map + 可替换接口)
=== .trellis/spec/backend/rate-limiting.md ===
(完整规范:算法、429 响应格式、禁止项、文件结构)
=== .trellis/tasks/08-10-rate-limiting/prd.md (Requirements) ===
(完整需求 + 验收标准)
## Your Task
实现 /api 速率限制

Codex:手动注入(替代方案)

Codex 没有自动注入的 Hook,所以这步你手动保证三份文档被读到。两种等价做法,选其一:

  • 做法 A(推荐,最省心):先把规范读进上下文,再下任务。

    1
    $before-backend-dev          # 读取后端 spec 索引 + 相关规范

    然后 点名 补上 before-dev 默认不读的两份文件,再下任务:

    1
    2
    3
    你:实现前先读 CONTEXT.md、docs/adr/0001-in-memory-rate-limiter-counter-store.md、
    .trellis/spec/backend/rate-limiting.md,然后按 .trellis/tasks/08-10-rate-limiting/
    开发 /api 速率限制。
  • 做法 B(一劳永逸):把”必读清单”写进 Codex 的 implement agent 配置 .codex/agents/implement.toml,让每次实现都默认读 CONTEXT.md + docs/adr/ + 任务 spec。配一次,以后免点名。

implement.jsonl 在 Codex 上的角色就是这份”必读清单”——你照着它点名,效果和 Claude Code 自动注入一致。

两平台共同的产物:代码

implement agent 写出来的代码(完整、可运行):

src/middleware/rateLimit.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
class TokenBucket {
constructor({ capacity, refillPerSecond }) {
this.capacity = capacity;
this.refillPerSecond = refillPerSecond;
this.tokens = capacity;
this.lastRefill = Date.now();
}
consume(count = 1) {
this._refill();
if (this.tokens < count) return false;
this.tokens -= count;
return true;
}
timeUntilAvailable(count = 1) {
this._refill();
const deficit = count - this.tokens;
if (deficit <= 0) return 0;
return Math.ceil(deficit / this.refillPerSecond);
}
_refill() {
const now = Date.now();
const elapsed = (now - this.lastRefill) / 1000;
this.tokens = Math.min(this.capacity, this.tokens + elapsed * this.refillPerSecond);
this.lastRefill = now;
}
}

// 进程内存储,见 ADR-0001;导出以便未来替换为 Redis
const buckets = new Map();
function getBucket(key, { capacity, refillPerSecond }) {
let b = buckets.get(key);
if (!b) { b = new TokenBucket({ capacity, refillPerSecond }); buckets.set(key, b); }
return b;
}

function defaultKey(req) {
return (req.user && req.user.id) || req.ip;
}

function rateLimit({ capacity = 60, refillPerSecond = 1, keyFn = defaultKey } = {}) {
return (req, res, next) => {
const bucket = getBucket(keyFn(req), { capacity, refillPerSecond });
if (bucket.consume()) {
res.setHeader('X-RateLimit-Limit', capacity);
res.setHeader('X-RateLimit-Remaining', Math.floor(bucket.tokens));
return next();
}
const retryAfter = bucket.timeUntilAvailable();
res.setHeader('Retry-After', retryAfter);
res.setHeader('X-RateLimit-Limit', capacity);
res.setHeader('X-RateLimit-Remaining', 0);
return res.status(429).json({ error: 'rate_limit_exceeded', retryAfter });
};
}

module.exports = { rateLimit, TokenBucket, _buckets: buckets };

src/app.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const express = require('express');
const { rateLimit } = require('./middleware/rateLimit');

const app = express();

// /health 直接挂在 app,不经过 /api → 天然豁免限流
app.get('/health', (_req, res) => res.json({ ok: true }));

// /api 子路由启用限流
const api = express.Router();
api.use(rateLimit({ capacity: 60, refillPerSecond: 1 }));
api.get('/users', (_req, res) => res.json([{ id: 1, name: 'Alice' }]));
app.use('/api', api);

module.exports = app;

注意代码和文档的对应关系:X-RateLimit-Remaining429 响应体、可替换的 keyFn、导出的 _buckets——全是 spec 和 ADR 里定的。不是 agent 随机发挥,是它 读到了 那三份文件(Claude Code 靠 Hook 自动读,Codex 靠你点名读)。


7. 让质量门把关(Step 5)

implement agent 报”完成”后,要验证代码确实满足 spec、且 lint/type/test 全绿。这一步两平台 机制不同

先配验证命令(两平台都写这份文件):

.trellis/worktree.yaml

1
2
3
4
verify:
- npm run lint
- npm run typecheck
- npm test

Claude Code:Ralph Loop 自动把关

Trellis 自动调 check agent,ralph-loop.py 在 check agent 想停的时候拦截,按 worktree.yaml 的 verify 跑命令。假设第一次 check 跑挂了——比如 implement agent 忘了把 /health 挂在 /api 外面,导致集成测试里健康检查也被限流,测试红:

1
2
3
4
5
check agent: 我做完了
ralph-loop.py: 跑 npm test → 失败(健康检查被限流)→ 拦截,不让停
check agent: 收到失败信息 → 读 check.jsonl 里的 rate-limiting.md「豁免在路由层声明」
→ 把 /health 移出 /api → 重跑 npm test → 过
ralph-loop.py: lint/type/test 全绿 → 放行

如果没配 verify,Ralph Loop 会退化成 Mode B(completion markers)——从 check.jsonlreason 字段生成标记,要求 check agent 输出里包含这些标记才能停。程序化验证更可靠,优先用上面的 verify。

Codex:手动把关(替代方案)

Codex 没有 Ralph Loop,”lint/type/test 不过就自动打回”的环不存在。手动等价做法分两步:

  1. 核对 spec:跑 $check-backend,它重读 rate-limiting.md、逐条对比代码、自动修违规(这步两边都有)。
  2. 跑验证命令:自己执行 npm run lint && npm run typecheck && npm test;红了就把报错贴回去,再 $check-backend,直到全绿。

worktree.yaml 里的 verify 在 Codex 上不会被 Hook 自动跑,但它是你 手动的检查清单——照着跑就行。手动多按两下按钮,结果和 Ralph Loop 一样:代码得过这三关才算完。

两平台共同的产物:测试

集成测试(正是它抓住了上面那个错):

test/rateLimit.test.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const request = require('supertest');
const app = require('../src/app');
const { _buckets } = require('../src/middleware/rateLimit');

afterEach(() => _buckets.clear());

test(' 健康检查不受限流 ', async () => {
await request(app).get('/health').expect(200);
await request(app).get('/health').expect(200); // 连续两次都 200
});

test(' 超额返回 429 和 Retry-After', async () => {
const agent = request.agent(app);
for (let i = 0; i < 60; i++) {
await agent.get('/api/users').expect(200);
}
const res = await agent.get('/api/users').expect(429); // 第 61 次
expect(res.headers['retry-after']).toBeDefined();
expect(res.body.error).toBe('rate_limit_exceeded');
});

8. 收尾:完工检查 + 记录会话(Step 6)

/finish-work(Codex 是 $finish-work),Trellis 给一张六维清单:代码质量(lint/type/test)、文档同步、API 变更、数据库变更、跨层验证、手动测试。你按清单跑一遍,手动测一下:

1
2
3
4
5
6
7
curl -i http://localhost:3000/health        # 200,无限流头
curl -i http://localhost:3000/api/users # 200,带 X-RateLimit-*
# 连打 61 次,最后一次:
# HTTP/1.1 429
# Retry-After: 1
# X-RateLimit-Remaining: 0
# {"error":"rate_limit_exceeded","retryAfter":1}

确认没问题,你(不是 AI)来提交:

1
2
3
4
git add src/middleware/rateLimit.js src/app.js test/rateLimit.test.js \
CONTEXT.md docs/adr/0001-in-memory-rate-limiter-counter-store.md \
.trellis/spec/backend/rate-limiting.md .trellis/tasks/08-10-rate-limiting/
git commit -m "feat(api): add token-bucket rate limiting"

记录会话(让 AI 记住这次做了什么):

1
2
/record-session      # Claude Code
$record-session # Codex

它调 add-session.py,把这次工作追加到 .trellis/workspace/ 你的名字 /journal-N.md

第二天 回来,开新会话。两平台的 session-start.py 都会读 journal,AI 接上:”昨天你完成了速率限制(commit abc1234),令牌桶、进程内存储、429 响应。今天想做什么?”——决策跨会话存活了。


9. 固化(Step 7):让这套流程成为默认

跑通一遍后,把它变成项目默认,下次不用手动触发:

  1. ** 替换 /brainstorm**(Codex 是 $brainstorm):Trellis 自带的 brainstorm 命令干规划阶段的活。把它的内容改成”调用 grill-with-docs”,于是每个新任务进入规划阶段自动走逐题审问。grill-with-docs 比 brainstorm 多了”逐题、给推荐答案、只问决策、边问边记文档”四条硬规则,是它的加强版。
  2. 把 glossary / ADR 挂进默认模板:在 backend/index.md 或任务 JSONL 模板里默认带上 CONTEXT.mddocs/adr/ 的引用,省得每个任务手写 implement.jsonl 时忘加。Codex 用户更省事——直接写进 .codex/agents/implement.toml,一次配好永久生效(见 Step 4 做法 B)。
  3. 给 grill 定门槛:不是每个任务都值得完整审问。用 ADR 三条件当闸门——改个按钮颜色跳过,搭鉴权 / 限流这种才跑完整 grill。

10. 新手踩坑清单

  1. 在 Codex 上期待全自动:以为命令一敲,spec 自动注入、测试自动把关。→ Codex 缺 PreToolUse 注入 Hook 和 Ralph Loop,Step 4 必须手动点名读文档、Step 5 必须手动跑验证。要全自动,用 Claude Code;用 Codex 就接受这两步手动。
  2. 命令前缀用错:在 Codex 上打 /start 没反应。→ Codex 用 $,Claude Code 用 /。记法:/xxx$xxx
  3. grill 一次问一堆:AI 把五个问题一次抛出来让你懵。→ 违反 grilling 规则,让它”一次一题、每题先给推荐答案”。两平台都适用。
  4. CONTEXT.md 混进实现细节:审到”用参数化查询”顺手写进术语表。→ 那是 spec 的活,拉回来;glossary 只装业务词汇。
  5. 三份文档没进 implement agent 的视线:审得很彻底,但写出来的代码完全没遵守。→ Claude Code 上多半是 CONTEXT.md / ADR / spec 没写进 implement.jsonl;Codex 上多半是 Step 4 忘了 $before-backend-dev + 点名。注入不到 / 没读到,决策白审。这是最常见的失败。
  6. 什么任务都全量 grill:小改动也走七题审问,团队开始绕过它。→ 用 ADR 三条件(难推翻 + 没上下文会困惑 + 真权衡)当门槛,只有值得的才全量审。
  7. Codex 上忘了跑验证:以为 $check-backend 跑完就万事大吉。→ 它只核对 spec,不替你跑 lint/type/test。那三条命令(或 worktree.yaml 的 verify)你得自己按,红了再修。

11. 换成你的真实需求:值不值得走完整 grill?

你刚拿速率限制走了一遍完整流程。但不是每个需求都该这么干——grill 的真实成本是 人得在线一题一题答 。所以判断的核心不是”这功能重不重要”,而是” 提前问清楚能省下的返工,值不值得你花这 20 分钟在线“。

返工成本高(决策难推翻、跨层、领域有歧义)→ 值得。返工成本低(单文件、可逆、无歧义)→ 不值得。

四个问题自测(每个”是”计 1 分):

  1. 可逆性:做错了,改回来的代价大吗?(比如已对外、客户端已依赖响应格式)
  2. 意外性:半年后回来看这段代码,会问”为什么这么做”吗?
  3. 歧义性:需求里有”一个词,团队三个人三个意思”的术语吗?
  4. 跨层性:它会同时碰前端 / 后端 / 数据库 / 外部系统里的两个或以上吗?

按分数分三档

分数 档位 怎么做 在 Trellis 里落法
≥3 全量 grill 完整逐题审问,写 glossary + ADR + spec CONTEXT.md + docs/adr/ + spec,全挂进 implement.jsonl
1–2 轻量 grill 只审 2–3 个关键决策,写 spec 条目,不写 ADR 只更新相关 spec 文件,任务照常建
0 跳过 不 grill,直接描述需求 直接走 Task Workflow,让 Plan agent 评估(需求不清它会拒绝并让你澄清)

ADR 三条件(难推翻 + 没上下文会困惑 + 真权衡)就是上面第 1、2 问再加一条”有没有真备选”——分数高且满足三条件的决策才值得写 ADR。

常见需求归档(拿来对照):

  • 全量:支付回调、权限模型、订单 / 工单状态机、限流 / 熔断、多租户隔离、搜索 / 排序 / 分页口径、缓存策略、批量导出。共同点:决策多、难推翻、跨层、术语有歧义。
  • 轻量:新增一个 CRUD 资源(表结构已清楚)、给接口加筛选参数、加日志埋点、给一个查询加缓存。共同点:1–2 个值得记的点,但领域清楚。
  • 跳过:改文案 / 样式 / 常量、重命名、加可选字段、调阈值、修明显 bug。共同点:可逆、单文件、无歧义。

两个最容易踩的误判

  • “这是新功能” ≠ 要全量 grill。 一个全新的 CRUD 资源,字段和表结构都清楚、领域无歧义,就是轻量甚至跳过。新不等于难。
  • “代码量小” ≠ 该跳过。 一个几行的改动只要碰了状态机边界或鉴权逻辑(比如”取消订单”到底允不允许部分取消),就该全量。判断看 决策密度,不看代码行数。

一句话记住:grill 治的是”返工”,不是”工作量”。


附录 A:还没有后端项目?真能跑通的最小 scaffold

建一个目录,放四个文件,npm installnpm test 直接通过。下面这版我在 Windows + Node 24 上实测过:npm test → 2 passed,npm run lint / npm run typecheck 都返回 0,npm start 起服务后 curl /health 拿到 200。两平台通用(scaffold 本身和 AI 工具无关)。

package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"name": "rate-limit-demo",
"version": "1.0.0",
"private": true,
"scripts": {
"start": "node src/server.js",
"test": "jest --runInBand",
"lint": "node -e \"console.log('lint: placeholder OK')\"",
"typecheck": "node -e \"console.log('typecheck: placeholder OK')\""
},
"dependencies": {
"express": "^4.19.2"
},
"devDependencies": {
"jest": "^29.7.0",
"supertest": "^6.3.4"
}
}

lint / typecheck 占位脚本,永远返回 0——它们存在的唯一目的是让 Step 5 里质量门的 npm run lint && npm run typecheck && npm test 链子从第一天就能端到端跑通(Codex 上是你手动跑这三条,Claude Code 上是 Ralph Loop 自动跑)。等你接了真实的 eslint / tsc,把这两行换掉即可。

src/app.js

1
2
3
4
5
6
7
8
9
const express = require('express');

const app = express();

// 这就是 grill 里 research agent 会去翻的「现有 API」
app.get('/health', (_req, res) => res.json({ ok: true }));
app.get('/api/users', (_req, res) => res.json([{ id: 1, name: 'Alice' }]));

module.exports = app;

**src/server.js**(让 npm start 能起服务,供 Step 6 的 curl 验证)

1
2
3
4
5
6
const app = require('./app');

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`listening on http://localhost:${PORT}`);
});

test/app.test.js

1
2
3
4
5
6
7
8
9
10
11
const request = require('supertest');
const app = require('../src/app');

test(' 健康检查返回 200', async () => {
await request(app).get('/health').expect(200);
});

test('/api/users 返回用户列表 ', async () => {
const res = await request(app).get('/api/users').expect(200);
expect(res.body[0].name).toBe('Alice');
});

装依赖并验证:

1
2
3
4
5
npm install
npm test # → Tests: 2 passed
npm run lint # → lint: placeholder OK
npm run typecheck # → typecheck: placeholder OK
npm start # 另开终端:curl http://localhost:3000/health → 200

三个脚本都过、服务能起,说明工程骨架是好的。然后初始化 Trellis(按平台二选一):

1
2
trellis init -u 你的名字            # Claude Code
trellis init --codex -u 你的名字 # Codex

再从 Step 1(装 grill-with-docs)开始。

说明:这里的 src/app.js 初始状态 (扁平路由、无限流)。Step 4 的 implement agent 会把它 重构成 /api 子路由并挂上限流中间件,同时新建 src/middleware/rateLimit.js——所以你在 Step 4 看到的 src/app.js 是重构后的样子,不是从零写的。

附录 B:三个 skill 文件完整内容

两个平台用的是 同一份内容,只是存放位置不同:Claude Code 放 .claude/skills/,Codex 放 .agents/skills/

grill-with-docs/SKILL.md

1
2
3
4
5
6
7
---
name: grill-with-docs
description: A relentless interview to sharpen a plan or design, which also creates docs (ADR's and glossary) as we go.
disable-model-invocation: true
---

Run a `/grilling` session, using the `/domain-modeling` skill.

grilling/SKILL.md

1
2
3
4
5
6
7
8
9
10
11
12
---
name: grilling
description: Grill the user relentlessly about a plan, decision, or idea. Use when the user wants to stress-test their thinking, or uses any 'grill' trigger phrases.
---

Interview me relentlessly about every aspect of this until we reach a shared understanding. Walk down each branch of the decision tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer.

Ask the questions one at a time, waiting for feedback on each question before continuing. Asking multiple questions at once is bewildering.

If a *fact* can be found by exploring the environment (filesystem, tools, etc.), look it up rather than asking me. The *decisions*, though, are mine — put each one to me and wait for my answer.

Do not act on it until I confirm we have reached a shared understanding.

domain-modeling/SKILL.md

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
---
name: domain-modeling
description: Build and sharpen a project's domain model. Use when the user wants to pin down domain terminology or a ubiquitous language, record an architectural decision, or when another skill needs to maintain the domain model.
---

# Domain Modeling

Actively build and sharpen the project's domain model as you design. Challenge terms, invent edge-case scenarios, and write the glossary and decisions down the moment they crystallise.

## File structure

/
├── CONTEXT.md ← glossary, ONLY terms, no implementation details
├── docs/adr/ ← architectural decision records
└── src/

Create files lazily — only when you have something to write.

## During the session

- Challenge against the glossary: if a user's term conflicts with CONTEXT.md, call it out.
- Sharpen fuzzy language: propose a precise canonical term.
- Cross-reference with code: if stated behavior contradicts the code, surface it.
- Update CONTEXT.md inline the moment a term is resolved. CONTEXT.md is a glossary and NOTHING else — totally devoid of implementation details.
- Offer an ADR only when ALL three are true:
1. Hard to reverse — changing your mind later costs meaningfully
2. Surprising without context — a future reader will wonder "why?"
3. The result of a real trade-off — there were genuine alternatives
If any is missing, skip the ADR.