OpenSpec + Superpowers,加了 Harness:我只定义需求,中间什么都不需要

0 阅读9分钟

Anthropic 工程团队发了一篇关于长任务 Harness 设计的文章。有一句话让我停下来:

"Generators self-assess poorly — confident praise even for mediocre output."(生成器自我评估很差——即使是平庸的输出,也会自信地称赞自己。)

我一直在 OpenSpec 的 apply 阶段内置了代码审查技能。它在和实现相同的 agent context 里运行。审查者和实现者是同一个 session。看到这句话我立刻明白:这就是我的问题。审查者在刚写完代码的 context 里运行,充满了自信,看不到偏差。

问题找到了,以下是我的增强。


那个早就存在的缺口

上一篇文章里,我修了五个问题。其中第三个增强把 superpowers:requesting-code-review 直接内置到 /opsx:apply 里每个任务组的执行流程中。在那之前,代码审查是我得记得主动发起的事;增强3之后,它会自动触发。

这个改动是对的。但它没有解决根本问题。

审查在 apply agent 的 context 里运行。那个 agent 刚刚实现了功能,它推理过每一个设计决策,写了每一个测试,亲眼看着它们通过。它审查自己的代码时,带着对「这段代码是怎么写的」的完整认知。Anthropic 的发现说得很准:一个自我评估的生成器,只会确认自己的意图,不会发现自己的错误。

实际症状:审查结果回来是「没有 critical 或 high 问题」,然后我 merge。有两次,审查在技术上是正确的,但还是漏掉了东西——因为审查者和实现者共享了同一个视角。我需要的不是一个更聪明的审查者,而是一个根本不知道实现者意图的审查者。


Harness 的设计

我做了结构改变:把评估完全移出实现者的 context。

新设计在 apply 里加入了每组一次的 harness 循环

Group N:

  [N.0 CONTRACT]
    apply agent writes contracts/group-N.md
    content: spec SHALL statements, runtime test commands, code review points, threshold

  [N.1 RED → N.X GREEN]
    TDD as before

  [N.E EVAL]  ← replaces the old requesting-code-review step
    spawn evaluator subagent — fresh context, haiku model
    context: contract file + spec + design + git diff (group only)
    evaluator:
      1. invoke superpowers:requesting-code-review
      2. run runtime test commands from contract
      3. diff implementation vs spec SHALL statements
      4. aggregate score: Spec (40%) + Runtime (40%) + Code (20%)
      5. write to eval-log.md
      6. return BLOCK | PASS | RETRY

  on RETRY: append FIX tasks, re-run N.E (up to 3 attempts)
  on BLOCK: pause immediately, report to human

和旧的审查步骤相比,有三个关键差异。

全新的 context。 evaluator subagent 启动时对代码是怎么写的一无所知。它读 contract 文件,读 spec,读 diff,仅此而已。它不会被实现者的意图影响,因为它从没看过实现者的推理过程。

显式 contract。 实现开始之前,apply agent 先写好 contracts/group-N.md——这是一个扁平的清单,定义这组任务的「完成」是什么:哪些 spec SHALL 语句适用、要运行哪些测试命令、要验证哪些设计决策。evaluator 读的是这份 contract,而不是实现者脑子里对 contract 的记忆。

结构化评分。 PASS/FAIL 不够用。evaluator 在三个维度上给出带权重的评分。只要出现任何 CRITICAL 或 HIGH 的代码问题,无论总分多少,立即触发 BLOCK。总分低于阈值则触发 RETRY。分数 78 的 RETRY 和分数 55 的 RETRY 信号是不一样的——信号的强度和问题的严重程度成正比。

openspec+harness.png


实际发生了什么

我在 python-agentmulti-user-auth-admin-ui 变更上跑了这套流程:一个管理用户的 Admin UI,支持邀请、停用、重新启用、删除,带二次确认输入门。八组任务,七十二个任务,八次 evaluator 运行。

multi-user-ui.png

平均分 98.3。六组一次通过,两组各重试了一次。

最关键的重试是 Group 3。

Group 3 覆盖 Pinia store——调用后端 API 端点的前端状态层。evaluator 返回分数 78、状态 RETRY,附带这条发现:

"HIGH SEVERITY — all 5 actions call endpoints without /api prefix (e.g. /admin/users instead of /api/admin/users). Backend route is /api/admin/users. Mocked tests pass but real API calls will 404."

七个测试全部通过了。因为 Axios 被 mock 了,mock 测试不验证实际的请求路径。实现者的审查确认了测试套件是绿的。evaluator——读着 contract,读着 spec,对照实际的后端路由——发现了实现者从内部看不到的东西。

没有 harness,这个 store 就会被 merge。Admin UI 的每一个 API 调用都会在生产环境默默 404。

另一个发现来自 Group 2 的代码审查维度。evaluator 发现 invite 在用 request.host_url 拼接邀请链接的 URL——这是 API 服务器的 host。我的 NAS 环境里,API 和 Web UI 跑在不同的端口上,每一条发给用户的邀请链接都会带错误的端口。解法是用 APP_BASE_URL 环境变量。自我审查会看到这段代码,看到它在测试里能跑通,然后继续往下走。evaluator 只读 contract 和 spec,看出这个实现方式对部署目标来说是错的。

八组任务,新增九十九个测试,两个 HIGH 问题被一个根本不知道「我以为代码已经做完了」的 evaluator 捕获。

完整 eval log 在这里


成本模型

加一个 evaluator 意味着多烧 token。这次跑的实际数字:

  • 总 token:约 58M
  • 缓存命中:约 56M(96%)
  • evaluator 模型:haiku

evaluator 的 context 刻意设计得很小。它看的是 contract 文件、相关 spec 章节、设计文档、以及只属于那一组的 git diff——不包括 apply agent 的对话历史。小 context + haiku 模型,每次跑的成本很低。单组最坏情况是三次 evaluator 运行,实际上大多数组第一次就过了。

96% 的缓存命中率是控制整体成本的关键。apply agent 跨八组积累 context,但大部分 context 是稳定的——spec 文件、设计文档、前几组已完成的工作。haiku evaluator 每次评估组任务都命中同一份缓存的 spec。结构化文档加低变动率,正是 prompt caching 设计的使用场景。


不需要我做的事

这个功能的 explore 是十六天前做的——读代码库、起草需求、画 mock。之后搁在那里没动。今晚跑了 /opsx:propose/opsx:apply,一气呵成。八组任务,七十二个任务执行完毕,九十九个测试写好并通过,两个 HIGH 问题被发现、修复、重新评估。最后冒烟测试:邀请流程通了,二次确认流程通了,Admin UI 和 mock 对得上。

我定义了需求。我做了冒烟测试。中间什么都没做。

这不是一个「AI 什么都做了」的故事,而是一个「工作流知道完成意味着什么」的故事——因为 contract 定义了它,evaluator 执行了它,而两者都不需要我盯着才能工作。

这套工具链的早期版本,在审查步骤上需要我的判断。我得读 diff,决定什么重要,决定 spec 是否被满足。其中一部分是自律,大部分是在完善一个不靠我就说不清楚「完成」是什么的工作流。harness 没有替代那个判断——实现开始之前我写的 contract 本身就是判断。但一旦 contract 存在,验证循环就不再需要我。

人在开发Loop中变成了:提前定义完成是什么,最后验证它发生了。只有头和尾


Agile,更深一层

上一篇文章把四个命令映射到了 Agile 原则——explore 是规划,propose 是「可工作的软件作为进展的度量」,apply 是测试驱动,archive 是回顾。这些映射依然成立。harness 的演进没有改变命令,它改变的是 apply 内部发生的事情。

三个 Agile 原则变得更完善了。

Definition of Done。 Agile 团队在 sprint 开始前写验收标准。现实是,验收标准在 ticket 里,ticket 在 Jira 里,Jira 不是 Claude 读的东西。N.0 CONTRACT 在实现开始前写的 contract 文件,是 evaluator 实际能用的验收标准形式。「完成」不再是「测试是绿的」,而是「evaluator 对这份具体的 contract 打分超过阈值」。这是一个实质性不同的标准。

小迭代。 原来的 apply 流程按任务跑 TDD,但反馈循环在最后关闭:整组一次代码审查,一次人工决策。harness 循环按组自动关闭。每组是一个微型 sprint:定义、实现、评估、需要的话重试、推进。迭代之所以小,不是因为我计划了,而是因为 evaluator 在组边界触发,不让组向前推进直到分数通过。约束是结构性的。

回顾。 eval-log 是回顾记录。八组跑完,我可以读到哪些组需要重试、evaluator 发现了什么、生成了哪些 FIX 任务、最终哪个分数通过了。archive 阶段读 eval-log 来识别 CLAUDE.md 的潜在陷阱——重试超过一次的组是自动候选。这正是 Agile 回顾的模式:哪里出了问题,带什么往前走。区别在于 eval-log 在工作进行时自己写好了,而不是 sprint 结束后凭记忆整理。

上一篇的结论是:约束内置到工作流本身,不管我记不记得触发它,它都会运行。harness 更进一步:约束在运行,运行的证据也被保留下来。不在对话记忆里,在文件里。


当前状态与下一步

harness 验证的变更在 opsx-superpowersfeat/harness-validation 分支上,我现在正在用它跑 apply。

如果这个模式在接下来几次变更中都没问题,我会开 PR 合并到 main。

你可以在你的项目上试用:

# 直接安装 harness 分支
claude --plugin-url https://github.com/austinxyz/opsx-superpowers/tree/feat/harness-validation

# 提升 schema——在项目根目录执行一次,每次安装/升级后执行
opsx-install

# Windows (PowerShell): 用 ! 前缀
# ! bash /c/Users/<你的用户名>/projects/opsx-superpowers/bin/opsx-install

然后按原来的方式运行:/opsx:explore/opsx:propose/opsx:apply/opsx:archive。contract 步骤在每组开始时触发,evaluator 在每组结束时触发。需要重试的组会自动重试,触发 BLOCK 的组会暂停等待。eval log 写入 openspec/changes/<topic>/eval-log.md/opsx:archive 运行时随其他内容一起归档。


参考链接

系列文章:

本文引用:


方法论没有完成。也不会完成。这依然是重点。