
从想法到发布:Harness Engineering 的一次完整实践
TLDR
《Harness Engineering 又来颠覆了》提出了三条原则,《蒸馏 Google 工程基因》介绍了 agent-skills——但「状态持久化」那条原则一直没有落地答案。这篇用一个真实项目 jk 走完一次:idea-refine 把模糊需求逼出三个核心设计决策,OpenSpec 把决策切成 17 组可执行任务并跨越多次 context 压缩,agent-skills 在关键节点自动介入。从「我想做个 Jenkins CLI」到 v0.1.0 发布,每一步都有凭据。
一、两个未竟的问题
《Harness Engineering 又来颠覆了》结尾留了一个悬念:AI coding 不会替你省掉软件工程的基本功,它会让你欠的债加倍奉还。三条债:可见性、状态持久化、质量门禁。但怎么还,没说。
《蒸馏 Google 工程基因》给出了一半答案:agent-skills 把高级工程师的判断力编码成 20 个结构化 skill,覆盖了质量门禁(TDD、code review、显式化假设),也覆盖了可见性(spec-driven-development 把假设和边界写进 repo,跟代码一起演化)。但有两个问题没有解决:
状态持久化没有答案。 agent-skills 管的是「一次任务怎么做」,管不了「多次 session 之间项目状态怎么延续」。agent 没有记忆,context 会压缩,上一次做到哪里、踩了什么坑、下一步是什么——skill 本身解决不了这个问题。
没有一次完整的实践记录。 《蒸馏 Google 工程基因》讲的是 agent-skills 是什么、怎么设计的。但实际用起来是什么感觉,两个工具碰在一起会发生什么,从来没有跑通过一次。
OpenSpec 是缺失的那块。它的核心设计是 spec 与 change 分离:spec 是意图载体,跨 session 稳定;change 记录执行状态,append-only。agent 每次进来,读 spec 知道要做什么,读 change 知道做到哪了。context 可以压缩,项目状态不会丢。
加上 agent-skills,三条原则才有了完整的覆盖。jk 是这套组合跑通的一次真实记录——一个 Pipeline-native 的 Jenkins CLI,从「想法」到「v0.1.0 发布 + Homebrew tap 自动更新」,全程用这套工作流完成。
二、从一句话到三个设计决策
「我想做一个 Jenkins CLI 工具。」
agent 拿到这句话会写出东西——但大概率不是你要的那个。它没有说为什么现有工具不够用,没有说用户是谁、在什么场景下用、什么叫做完。
这是 idea-refine 存在的原因。它不是头脑风暴工具,是一个把隐藏假设逼出来的结构化对话流程:先发散、再收敛、最后输出一份可以直接驱动后续工作的 one-pager。
在 jk 的开发里,idea-refine 经历了三轮关键交互,每轮都有一个具体的转折:
第一轮:「替代浏览器 Tab」和「快速出结果」之间的张力。
agent 直接点出来:这两个目标会打架。真正「替代浏览器 Tab」需要宽泛的功能覆盖,「快速出结果」需要窄而深。这是 jk 的第一个实质性决策:不做通用工具,只做 Pipeline operator 的日常工具——触发、监控、看日志、回应 input step。
第二轮:URL-as-identity。
「你怎么指定要操作哪个 Jenkins 实例?」这个问题的常见答案是 profile、context、配置文件。但实际使用习惯是:直接粘贴浏览器里的 Jenkins URL。agent 从这个行为模式里提炼出了核心设计哲学:URL 即身份。每个命令以 Jenkins URL 为输入,hostname 隐式决定使用哪份凭证,无 profile、无 context、无 use。
这不是一个功能,是整个 CLI 的交互范式。
第三轮:schema 设计反转。
最初的设想是「原样反映 Jenkins API 的响应」。用户提出:「我无法控制 Jenkins 版本。」agent 意识到这意味着什么——Jenkins 升级改字段,所有依赖 jk 输出的脚本集体崩溃。当场反转:jk 维护自有 schema,由 jk 的版本号控制,不随 Jenkins 升级漂移。--raw 作为逃生舱留着。
schemaVersion: "1" 出现在每条命令输出的第一行,是这次反转留下的直接痕迹。
三轮之后,「我想做一个 Jenkins CLI」变成了可以驱动实现的设计约束。这些约束直接体现在最终产品里:jk auth add 只需要一个 URL,不需要给实例起名字、不需要选 profile。

idea-refine 做的事,对应的正是《Harness Engineering 又来颠覆了》里的可见性原则:「Agent 看不见的就不存在。」这三个设计决策如果没有在开始就显式化,会在实现阶段以 bug、返工、和「这不是我想要的」的形式还回来。
三、两套 spec 打架的那一秒
idea-refine 的产出是一份 one-pager,OpenSpec 的 /opsx-propose 负责把它拆成可执行的 artifacts。
/opsx-propose 生成了四类文件:
proposal.md——做什么、为什么design.md——10 个技术决策- 7 份
specs/*/spec.md——auth、build、errors、output、pipeline、tls-and-transport、url-resolution tasks.md——17 组、约 80 个 checkbox,把整个实现切成可执行的最小单元
artifacts 创建完成后,手动触发了 spec-driven-development。
然后问题出现了。
spec-driven-development 的第一步是写项目级 spec——一份定义「在这个 repo 里怎么干活」的文档:构建命令、目录布局、代码风格、边界规则。但 OpenSpec 刚刚已经生成了 7 份 specs/*/spec.md。两套文档,都叫 spec,放在同一个 repo 里。
agent 加载 skill 后的第一个输出,直接处理了这个冲突:
We’ve completed the OpenSpec flow (proposal/design/specs/tasks), but the project-level spec that spec-driven-development calls for is a different dimension of document — it’s not about what capabilities we’re building, but about how we work in this repo (build commands, layout, style, guardrails).
The two are complementary, not conflicting:
- OpenSpec specs = what the product does (capabilities & requirements)
- Project spec = how we work (toolchain, layout, style, guardrails)
裁决结果:SPEC.md 放在 repo 根目录,作为「项目宪法」;openspec/specs/ 管产品行为;两者边界从此清晰,永不合并。
这条边界在开发过程中一直有效。一个具体的例子:开发后期把「credentials 加密留作 v0.2」这条决定从 SPEC.md 的 Open Questions 转为 Past Decisions——这是 engineering practice 层面的判断,改 SPEC.md,不动 OpenSpec。如果决定的是「新增 jk auth encrypt 命令」,那就必须开新的 OpenSpec change,因为那是 product behavior。
这条边界不是自然存在的。它是两个工具碰撞的那一秒,由 skill 即时裁决出来的。如果没有 spec-driven-development 的介入,SPEC.md 和 openspec/specs/ 很可能会在实现阶段互相打架——改了这边,那边过时;agent 下次进来,不知道以哪个为准。

四、tasks.md:让「下一步做什么」的决策成本归零
spec 的边界划清之后,剩下的问题是:怎么执行。tasks.md 就是答案。
/opsx-propose 生成的 tasks.md 里有 17 个任务组,约 80 个 checkbox。每个 checkbox 都是一个独立可执行的最小单元,有明确的完成标志。比如第 4.5 条:
Unit tests: every URL in
docs/spikes/urls.txtparses correctly; rejected URLs produce clear errors
不是「写单元测试」,是「这些输入,这些输出,测试通过才算完」。checkbox 的粒度就是验收标准的粒度。
agent 每次进来,第一件事是找下一个 - [ ]——不需要上下文重建,不需要问「我们做到哪了」。但更关键的是 context 压缩发生时的行为。
jk 的开发跨越了多次 session,context 被压缩了不止一次。agent 在 context 压缩时自动生成摘要,摘要里有一个固定的 Goal 段落,每次都完整保留了整个开发流程的标记:
Goal: Design, scaffold, and incrementally implement jk via
idea-refine → OpenSpec change → SPEC.md → code flow.
Current: Task group 9 (output rendering), 3 of 5 checkboxes done.
这一行是**「记忆脊柱」**。它不依赖 conversation history,不依赖外部工具,只要 tasks.md 在 repo 里,压缩摘要就能精确定位到「上次做到哪了」。
这正是 OpenSpec 的 tasks.md 与普通 TODO list 的本质区别:TODO list 的状态活在你的记忆或外部工具里;tasks.md 的状态活在 repo 里,跟代码一起被 git clone,被压缩摘要引用。context 可以压缩,项目状态不会丢。

jk build trigger 是 tasks.md 第 11 组的产物——trigger + 实时状态轮询 + 退出码等于 build 结果。这条命令能存在,是因为在 context 压缩了三次之后,agent 还能找到并完成它对应的 checkbox。
这是《Harness Engineering 又来颠覆了》里「状态持久化」原则最具体的落地形态。
五、没有人要求,但它自己加载了
实现 internal/jenkinsurl 是 tasks.md 第 4 组,共 5 个 checkbox:定义 Ref 结构体、实现 Parse()、HostKey()、APIPath(),以及单元测试。
任务描述里没有出现「TDD」这个词。
agent 开始动手之前,输出了这样一段判断:
Loading test-driven-development skill since this package has well-defined inputs/outputs and table-driven tests are the natural fit.
然后它加载了 test-driven-development skill,按 RED-GREEN-REFACTOR 顺序推进:先把 tasks.md 里「10 个真实 Jenkins URL 样本」的 spec scenarios 全部写成失败的测试,再让测试通过,最后清理实现。
没有人要求这样做。
skill 的作用不是提供步骤,而是让正确的 pattern 在合适的时机自动发生。agent 识别到「inputs/outputs 明确」这个信号,就知道该用哪套 pattern——不需要人来提醒,不需要 prompt 里写「请使用 TDD」。这正是《蒸馏 Google 工程基因》里说的:把高级工程师的判断力编码成 agent 无法绕过的结构性约束。
internal/jenkinsurl 是第一个,但不是唯一一个。整个 schema 层(internal/schema/、internal/output/)都是用同样的方式推进的——先把 spec scenario 里的字段写成测试断言,再让实现通过。
jk build status -o json | jq 的输出是结果:

每个字段在写实现之前就已经是测试的断言目标。TDD 驱动出来的不只是覆盖率,是 schema 稳定性。
质量门禁不是人来守的,是提前写进 skill 的信号识别逻辑里的。
六、v0.1.0 发布了,archive 还没做
tag v0.1.0 打了,CI 全绿,brew install addozhang/tap/jk 可用——从外面看,这个项目完成了。
openspec/changes/init-jk-jenkins-cli/ 这个 change 还没有 archive。
推迟的原因不是懒,是因为 archive 有一个前提:spec 被验证了。
/opsx-archive 的作用是把 change 里的所有决定——design.md 的 10 个技术判断、7 份 spec 的 scenario 定义——封存进主 spec,成为下一个 change 的基线。封存之后,这些决定就是既成事实——新的 agent 进来,读 spec,知道这些事已经决定了,不需要重新讨论。
这是 spec 的封存,不是功能的完成。
v0.1.0 的实现覆盖了 tasks.md 的 76 个 checkbox,4 个显式留到 v0.2。代码是完整的,但有几条 spec scenario 在真实 Jenkins 环境里还没有跑过——jk build logs -f 的流式输出,在测试环境里 mock 过,但没有对着生产 pipeline 用过。这是第一个要 dogfood 的命令。

等这条命令在真实环境里稳定跑起来,再开 /opsx-archive。
三条债,这是最后一条还清的时机——不是 tag 打了,而是 spec 的假设被现实确认了。
功能完成和 spec 封存,是两件事。
