返回文章

交付阶段:「完成」是一个状态,不是一句汇报

这是五阶段系列的第五篇,最后一篇。第一篇澄清阶段 把「完成」先写成契约,第二篇实现阶段 逐个任务把代码落下来,第三篇评测阶段 让独立的评判者按 rubric 打分,第四篇确认阶段 要求换一位评判者、用新证据连过若干轮。这一篇管最后一步:一个功能什么时候才有资格从「做完了」变成状态文件里的 done。母篇《从能跑到可交付:一套前端 AI 协作系统的设计笔记 》把五层约束与五阶段一起讲了,这个系列逐个拆开。我用的系统是开源的 frontend-harness-cli ,文中所有规则都能在仓库里找到对应实现。

评测给了 92 分,项目的构建却是红的

先看一个具体的场景。

功能是给订单列表加按状态筛选。开发者逐个任务做完,每个任务的代码检查都过,提交的 SHA 内核都验过。评判者按 rubric 逐条打分:功能点全中,92 分,通过。确认阶段换了另一位评判者,证据全部重新生成,又连过两轮。流程里每个人的结论都一样:做完了。

然后交付门禁跑项目自己的构建命令,红了。

原因在筛选功能之外:为了给表格加一列排序,开发者改了一个全站共用的表格组件,给它引了一个只能在浏览器里跑的库。这个组件撑着十几个页面,其中两个是纯服务端渲染的,构建时直接报错。功能页自己在开发环境里跑得好好的——它的问题不在自己身上,在它把项目弄坏了。

如果「完成」由开发者在任务清空时宣布,或者由评判者在打出 92 分时宣布,这个功能就带着一个红着的构建交付出去了。评测没有错,它回答的问题是「这个功能符不符合契约」;构建回答的是另一个问题。交付阶段存在的理由,就是在有人说「做完了」和系统承认「完成了」之间,再加一道只认事实的关口。

为什么「完成」只能由内核来改

流水线里有三方都握着一部分事实,没有一方握着全部。

开发者的记录到任务为止:哪个任务做完、提交是哪个 SHA。他说「做完了」,意思是任务清单清空了——而且按实现阶段的设计,他从头到尾没有见过 rubric,他根本没有资格判断功能合不合格。

评判者的记录到契约为止:rubric 逐条的分数和证据。但 rubric 是写代码之前定下的,契约里写漏的东西她无从打分;项目带着这个功能还能不能整体构建,也不在她的检查范围内。

确认阶段补上的是稳定性:换人、新证据、连过若干轮。它证明评测结论不是运气,但它证明的仍然只是契约之内的事。

所以「完成」不能是任何一方的结论,它是一个状态——state.json 里 phase 字段的值。能改这个值的只有内核,因为只有内核在改的那一刻,手里同时握着三份记录。它的转移规则是一条合取式,逐项核对记录:

  • 评测结论是通过;
  • 稳定轮次已经满足;
  • 交付门禁刚刚跑完,全绿。

三项同时成立,状态才从确认走到交付。少任何一项,状态停在原地——这套状态机里没有「基本完成」这个值。

由这条规则还带出一条纪律:state.json 不许任何人手改。手把 phase 写成 done,只是一个被改过的 JSON,下面没有评测结论、没有门禁结果垫着。干活的人的结论是内核的输入,不是判决;判决只发生在记录齐了的那一刻。

交付门禁跑的,是项目自己的命令

交付门禁跑什么?答案有点出人意料:它不跑 harness 自己的任何检查,它跑项目自己的命令。

每个项目在配置里写下自己的门禁命令(gateCommands),通常就是它 CI 里那几条:npm run build、完整的测试套件、npm run lint——项目自己定义的「什么样才算能上线」。

为什么不用 harness 的检查顶替?因为 harness 对一个项目怎么上线没有发言权。rubric 是按功能写的契约,扫描是通用的仪器,只有项目自己知道自己的发布标准是什么。交付阶段 harness 要做的不是再加一层意见,是在最后一刻拒绝放行:项目自己的门槛没过,哪儿也不许去。

还有两个执行细节。一是当场跑:门禁在状态转移之前对着当前这棵代码树执行,早上 CI 绿过不算数——那时功能还没合进来,那是另一棵树。二是范围和评测不同:rubric 回答「这个功能符不符合它的契约」,门禁回答「这个项目带上这个功能,还能不能整体构建、还能不能过它自己的测试」。跨功能的破坏——改坏一个共用组件、动了依赖弄坏别的页面——只会出现在第二个问题里。开篇那个红构建,就是从这两个问题之间的缝里漏出来的。

这一阶段的配套 skill 是 ship-checklist:一份在门禁前后逐项核对的清单——门禁命令是不是项目当前的那几条、构建跑的是不是最终的代码、报告生成了没有。它不做任何判断,只防漏项。

验收报告从状态里生成,不靠谁回忆

状态改成 done 的同时,系统用 harness report 生成一份验收报告。它不是让开发者在最后写一段总结——总结靠回忆,回忆会挑好的说。它从状态文件和历次评测记录里生成,内容三块:

  • 交付了什么:功能名、任务清单、每个任务对应的提交;
  • 分数是多少:评测的逐条结果、确认阶段的稳定轮次;
  • 什么没验过:被豁免的人工条目、跳过的 judge 项、扫描没有测到的维度。

第三块是这份报告的设计要点。一份只报喜的报告,读者分不清「验过了,没问题」和「根本没验」。这两件事在交付之后的风险完全不同:前者可以放心用,后者得有人盯着。把没验过的写出来,残余风险就摆在明面上,不再藏在某个人的记忆里。

也因为报告是从记录生成的,它可以复现:下周再对着同一份记录跑一次 harness report,得到的是同一份报告。它是记录的一个视图,不是一篇谁写的叙事。

交付之后:变化沉淀进 overview

老八阶段流程里有一个「文档沉淀」阶段,排在代码生成之后,把功能反向整理成文档。五阶段里它被取消了——不是文档不重要,是文档出现的位置变了:prd.md、tech-design.md 这些契约文件在澄清阶段就写好了,文档从第一天就在,不需要事后补写。

交付之后只剩一个文档动作:规划者执行 sync-overview,把这个功能带来的变化沉淀进项目的 overview 文件。但沉淀有过滤器:只收已经稳定下来的变化——真正交付了、并且留下来的产品与技术事实。写进 overview 的例子是「订单列表支持按状态筛选,规则是服务端过滤」这个级别的结论;没上线的尝试、被废弃的方案,不收。

为什么卡「稳定」这一条?因为 overview 是下一个功能澄清阶段的起点:规划者开工先读它,据此写新契约。把半成品和废案也写进去,下一个功能的契约就建在一份掺了假的地基上。反过来,一份跟得上现实的 overview,能让下一个功能的澄清不再考古:做「订单导出」时,不必先去代码里翻「现在到底能不能筛选」,overview 里有现成的答案。

文档在这套系统里不是一个阶段,是流程在每次交付的边界上留下的沉淀。

paused:预算用完,系统停下来等人

不是每个功能都能走到 done。这条流水线还有一个出口,叫 paused。

评测不过,功能退回实现阶段修,修完再评——这个回路有次数上限;单个任务的重试也有上限。这些预算是防失控的:没有上限,一个卡住的条目能把整条流水线拖进无限循环。当预算耗尽而功能还没通过时,内核不改 done,也不接着硬跑,它把状态改成 paused,停下来等人。

停得有据可查:状态文件里留着卡在哪个条目、用掉几轮、最后一次评测的结论。人回来接手时,不是面对一坨跑了一半的代码从头考古,而是拿着记录做决定——是计划错了,拆功能重来;是契约定得不合理,改 rubric 并把豁免写进记录;还是这个功能先搁置,去做别的。决定做完,流程可以从 paused 接着走。

paused 防的是两种更糟的结局。一种是无限循环:同一个条目修十几遍,每遍改法略有不同,钱和上下文都在烧,代码越修越乱,而且没有一个时刻有人被明确告知「这条路走不通」。另一种是悄悄降格:失败次数多了,不动声色地把标准放松一点让它过——交付的是没验过的功能,而且没有任何记录表明标准曾经动过。paused 逼着「没能做完」这件事以状态的形式摆在明面上,降格要降,也得由人签字、留记录。

所以 paused 不是失败状态,是这条流水线能说出的最诚实的一句话:以现在的契约和预算,这件事没能被做完。这句话本身,就是一种有用的交付。

这一阶段管不了什么

把边界说清。

第一,门禁的上限就是项目自己的命令的上限。一个测试覆盖几乎为零的项目,门禁全绿也说明不了多少。done 证明的是「这套系统跑过的检查全部通过」,不是「这个功能是对的」——检查单本身有多强,不在交付阶段的管辖范围内,那是澄清阶段写契约时的事。

第二,报告把没验过的列出来,不等于把它们验了。done 之后,盯住那份未验证清单仍然是人的工作。交付阶段改变的只是负担的位置:从「到底有没有人查过」变成「没查过的都在这儿,自己看着办」。

五个阶段到此走完:澄清、实现、评测、确认、交付。但阶段只是骨架,真正在每个阶段里干活的,是一份份被角色调用的 skill——澄清阶段那条著述链、评测阶段的 e2e-testing、这一阶段的 ship-checklist,都是这么来的。下一个系列把 21 份 skill 逐份拆开,第一份是整个流程的源头 clarify-requirements:它问问题的质量,决定了后面所有契约的质量。