# 通过 Trellis 来看 Harness 工程的实现

过去两个月我写了一系列关于 Harness Engineering 的文章。写到第三篇的时候我决定自己动手做一个，在项目里跑了三周，感觉非常不错，再也不会不敢清空上下文重新进入会话了。不过三周之后我在 GitHub 上遇到了一个叫 Trellis 的开源项目，用了一周就把自己的实现删掉了。

这篇文章不想再从原则出发往下推演了。反过来，从 Trellis 的具体技术实现往上看，聊聊它们各自揭示了 harness 工程的什么本质。

## 我做了什么

我那个实现叫 harness-templates，逻辑很简单：拷贝一组模板到新项目，CLAUDE.md 负责引入上下文。feature_list.json 追踪进度，WIP=1。每个会话开头读 progress 文档恢复上下文，做完写 session-handoff.md，提交 git，重开新会话。

它很简单，但是优点很鲜明，立即拥有了前所未有的可控感。

首先是透明。CLAUDE.md 里写什么 AI 就读什么，你清清楚楚知道它看到了哪些信息。出了问题排查很直接，去看 CLAUDE.md 就行。其次是零门槛。拷贝文件、改几行配置就能用，不需要理解任何额外的概念。第三是轻。整套东西加起来不超过十个文件，认知负担很低。

三周里我用它完成了两个中型功能的开发，跨会话的连续性确实比裸跑好了一截。

但也有两个反复出现的情况。一个是 AI 偶尔跳过 handoff 文档直接从 feature_list 里捡 todo 开干，完全不管上次做到哪了。CLAUDE.md 里写着"启动时先读 session-handoff.md"，但这只是一行建议，AI 有概率无视它。另一个是 400 多行规范全量塞进 CLAUDE.md，改一个三行的 API 返回值也得扛着全套前端组件规范、数据库命名约定、日志格式要求。

这两个问题当时我没太在意。直到看了 Trellis 的做法。

## Trellis 和它的设计准则

五月初在 GitHub 上看到 Trellis（github.com/daizhengxue/trellis），一个中国开发者做的项目，自称"开发工作流自动化引擎"，安装在项目的 .trellis/ 目录中。

比起直接看它的功能清单，先看它声明的五条设计准则更有意思：

1. **Plan before code**。强制在 planning 阶段完成需求收集和 PRD，才允许进入编码。状态机做硬性分割。
2. **Specs injected, not remembered**。AI 的 context window 是有限资源，规范应该在每次任务执行时精确注入，而非期望 AI "记住"。
3. **Persist everything**。对话会被压缩，但文件不会消失。所有决策、研究、教训都写入文件。
4. **Incremental development**。同时只有一个 active task，状态机强制执行。
5. **Capture learnings**。每完成一个任务，强制审视"这次学到了什么"并回写到 spec。

这五条跟我之前文章里总结的 harness 原则高度重合（Context 是稀缺资源、约束即自由、文件不会被压缩）。但关键的区别在于：我那个 harness-templates 用文字建议来落地这些原则，Trellis 用工程机制。下面逐个看它怎么做的。

## 技术亮点一：JSONL Curation 和 Spec 注入

对应准则："Specs injected, not remembered"

Trellis 的 .trellis/spec/ 目录按层级存放项目规范：frontend/、backend/、guides/，每个子目录下面是独立的 markdown 文件，比如 component-guidelines.md、error-handling.md、code-reuse-thinking-guide.md。

到这里还没什么特别的，这跟把规范拆成多个文件放进 .claude/rules/ 区别不大。关键在下一步。

每个任务目录下有两个 JSONL 文件：implement.jsonl 和 check.jsonl。格式长这样：

```jsonl
{"file": ".trellis/spec/backend/error-handling.md", "reason": "Auth error patterns"}
{"file": ".trellis/spec/guides/code-reuse-thinking-guide.md", "reason": "Check existing utils"}
```

当 AI 开始编码时，平台 hook 读取 implement.jsonl，把里面列出的 spec 文件内容注入 sub-agent 的 prompt。审查阶段读的是 check.jsonl，注入的是另一组文件。

这意味着做登录功能时，编码 agent 只看到 error-handling 和 code-reuse 两份规范。它不知道项目还有 component-guidelines.md 和 state-management.md，因为这两份跟当前任务无关，根本没出现在 implement.jsonl 里。

配 JSONL 确实比全量加载多了一道工作。但这个成本很快就能回收：第一个任务做完之后我观察到 AI 输出的代码跟规范的契合度明显比我之前的做法好，尤其是在命名约定和错误处理模式上。原因其实很直接——之前 400 行规范里只有 30 行跟当前任务有关，剩下 370 行都是噪音。噪音不是无害的。我在之前的文章里写过"每个 token 有 n² 的注意力计算成本"，Trellis 让我理解了这句话的实际含义：不相关的 context 会稀释相关信息的注意力权重。JSONL curation 做的就是一件事：只给 AI 看它该看的。

implement.jsonl 和 check.jsonl 分开这个设计也值得注意。编码 agent 需要"怎么写"的规范，审查 agent 需要"怎么查"的规范，两者有交集但不完全相同。分开 curation 让每个 agent 收到的 context 更聚焦。

## 技术亮点二：状态机和 Breadcrumb 注入

对应准则："Plan before code" + "Incremental development"

Trellis 的工作流是一个三态状态机：no_task → planning → in_progress。状态存在 task.json 的 status 字段里。

表面上看就是个状态字段，但它驱动的东西很多。每次用户发消息时，平台 hook 触发，调用 task.py current --source 获取当前任务状态，然后从 workflow.md 中截取对应的 breadcrumb 块注入到 AI 的 system prompt。

workflow.md 里用标记划分了不同状态的指令，大概长这样：

```
[workflow-state:planning]
Load the trellis-brainstorm skill and iterate on prd.md with the user.
Before task.py start, you MUST curate implement.jsonl and check.jsonl...

[workflow-state:in_progress]
Dispatch trellis-implement sub-agent with the curated spec context.
After implementation, dispatch trellis-check for quality review...
```

当状态是 planning 时，AI 只收到 planning 块的内容。它看不到 in_progress 块里写了什么，物理上不可见。所以它不可能跳过规划直接写代码，因为"写代码"这个指令对它来说不存在。

这跟我在 CLAUDE.md 里写"一次只做一个功能，先确认需求再动手"的区别在于：我的写法本质上是一个请求，AI 可以选择遵守或不遵守。Trellis 让"不遵守"这个选项从 AI 的认知空间里消失了。

这就是为什么"约束即自由"不是一句空话。当你通过信息不可见来约束每个阶段的行为空间，反而可以在该阶段内给 AI 更大的自主权。planning 阶段你可以放心让 AI 自由发挥去挖需求、做调研、写 PRD，因为它没有提前动手写代码的可能性。如果你只是写一句"先做计划"，你就不得不反复检查它有没有偷跑。

breadcrumb 机制还有一个隐含的好处：它让 workflow 的维护和 AI 的使用解耦了。你改 workflow.md 里某个状态的指令，所有后续的任务自动生效。不需要去改 CLAUDE.md，不需要通知 AI"规则变了"，因为每次 turn 它收到的都是最新版的 breadcrumb。

## 技术亮点三：Session Pointer 和跨会话恢复

对应准则："Persist everything"

这是让我最终决定删掉 harness-templates 的功能。

Trellis 在 .trellis/.runtime/sessions/ 下为每个 AI 会话窗口维护一个 JSON 文件：

```json
{
  "current_task": ".trellis/tasks/05-06-add-login",
  "platform": "claude",
  "last_seen_at": "2026-05-06T14:32:00Z"
}
```

context key（文件名）的解析有一套优先级：先看环境变量 TRELLIS_CONTEXT_ID，再看 hook input 里的 session_id，再看平台原生的 CLAUDE_SESSION_ID 或 CURSOR_SESSION_ID，最后 fallback 到"只有一个 session 文件就用它"。

每次用户发消息时 hook 触发，读取 session pointer，拿到 current_task，再去读 task.json 获取 status，再去 workflow.md 截取对应的 breadcrumb。整个链条是确定性的：hook 触发 → 读 pointer → 读状态 → 注入指令。没有任何环节依赖 AI 的"自觉"。

对比一下我那个 session-handoff.md 的做法。我的链条是：AI 启动 → AI 读到 CLAUDE.md 里"请先读 handoff 文档"的建议 → AI 决定是否去读 → 如果读了，从自然语言文本中理解上次进度。每一步都有失败概率。AI 可能跳过那行建议，可能读了但理解有偏差（自然语言总有歧义），可能两个窗口同时读同一个 handoff 文件导致冲突。

session pointer 把这整条链路从"AI 做决策"变成了"hook 做注入"。对话会被压缩，AI 的记忆会退化，但文件系统里的 JSON 不会。harness 文章里一直强调"把重要状态外部化到文件"，原因就在这里：文件是确定性的，不受 context window 压缩和 AI 注意力漂移的影响。

还有一个细节：多窗口支持。我用 Claude Code 做后端的同时 Cursor 里调前端样式，两个窗口各自绑定各自的任务。harness-templates 完全做不到这个，因为只有一个 session-handoff.md。

## 技术亮点四：Sub-agent 分离

对应准则："Specs injected, not remembered"（延伸应用）

Trellis 把编码工作拆成三种 sub-agent：trellis-implement（写代码）、trellis-check（审查代码）、trellis-research（调研）。三者运行在独立的 context 中，接收不同的 spec 内容。

implement agent 收到的是 prd.md + implement.jsonl 列出的编码规范。check agent 收到的是 check.jsonl 列出的质量标准 + 当前 diff。两者互不可见对方的 context。

为什么要这样分？因为"自己验自己"不可靠。

这个问题我在用 harness-templates 时也遇到过：AI 写完一段逻辑，你让它自己检查，它倾向于认为自己写得没问题。它知道自己的意图，所以会自动补全那些代码里其实没表达清楚的逻辑。这跟人类开发者自己 review 自己的代码一样不靠谱。

Trellis 的做法是让 check agent 带着质量规范去审查代码，但它不知道 implement agent 的"思路"。它只看代码和规范，这两者之间有没有 gap 就是它的判断依据。如果代码没有遵循 error-handling 规范里定义的异常处理模式，check agent 会标记出来并尝试修复，不会因为"理解了 implement agent 的意图"而放过。

这也是为什么 implement.jsonl 和 check.jsonl 要分开：让两个 agent 带着不同的视角看同一份代码。implement agent 的视角是"如何正确实现"，check agent 的视角是"是否符合标准"。context 隔离本身就是质量保证的一部分。

当然 check agent 不能替代真正的 CI/CD。它本质上还是 AI，如果 spec 有盲区它也发现不了。但比起单 agent 自审，多一个独立视角的可靠性好了一个量级。

## 放在一起看

这四个技术点各自解决一个具体问题，但放在一起能看到 harness 工程的几条底层逻辑。

最明显的一条：context 管理的核心在于"装什么"而非"装得下"。大模型的 context window 越来越长，但这不意味着你该把所有东西都塞进去。精选信息的成本永远值得付出。

状态机和 session pointer 指向同一个更深的判断：在 AI 系统中，确定性比智能更稀缺。文件系统是确定的，hook 触发是确定的，JSON 解析是确定的。好的流程控制靠让错误的选项从 AI 的认知空间里消失，把关键路径建立在这些确定性之上，效果比任何文字请求都好一个量级。

Sub-agent 分离则揭示了另一个维度的事：独立视角产生的质量增益来自一个简单条件，审查者不知道实现者的意图。信息隔离本身就是校验机制。

这些跟我之前文章里写的那些原则其实是同一批东西。但从具体实现往上看比从原则往下推演要立体得多。原则告诉你"应该这样做"，实现告诉你"为什么这样做有效，以及到底怎么做"。

## 代价和边界

Trellis 的主要成本在启动：认真填 spec 目录需要时间，每个任务的 JSONL curation 需要判断力。另外它有黑盒感，hook 塞了什么进去、sub-agent 实际读了哪些文件，用户看不到完整图景，出了问题排查成本比 CLAUDE.md 高。生态上，hook 自动加载只在 Claude Code 和 Cursor 上完整工作，其他平台需要手动补信息。

还有一个根本约束：spec 质量是价值上限。如果规范写得空泛，精确送入的就是一堆空泛内容，跟不筛选地灌进去也没差别。

所以我的用法是大功能走 Trellis 完整流程，日常小改动还是直接 CLAUDE.md 加 hook。

## 一句话

花三周搭的 harness-templates 被一周后发现的 Trellis 替代，根本原因是它在关键路径上用了机制而非建议。Harness 工程的价值在精度。想验证这个判断的话，去看看你 CLAUDE.md 里那些规则有多少是真正被执行的，多少只是写在那里图个安心。
