同步概览:长期记忆只收已经落定的变化
这是 Harness 系列的 skill 拆解篇,拆的这一份叫 sync-overview。它是实现、评测、交付这一组 skill 里的最后一份:交付阶段
里提过它一句——功能到达 done 之后,变化沉淀进 overview——这一篇把那一句展开。母篇《从能跑到可交付:一套前端 AI 协作系统的设计笔记
》讲整套系统的形状,这个系列逐份拆 skill:为什么有它,具体怎么干。我用的系统是开源的 frontend-harness-cli
,文中所有规则都能在仓库里找到对应实现。
一份过期三个版本的地图
先看一个具体的场景。
过去一个月,项目连着交付了三个功能:通知从一个个页面里拆出来,收进一个共用的消息中心模块;权限校验从前端挪到了服务端;导出功能多了一条异步任务队列。代码都上线了,评测都过了。
但项目的 overview 文件一次都没动过。它还写着:通知由各页面自行处理,角色在前端校验,导出是同步接口。
现在第四个功能开工。规划者是个全新上下文,它按规矩先读 overview——那是项目的官方记忆,白纸黑字。它信了。然后它照着这份记忆写技术方案:新通知复用"各页面自行处理"的老路子,权限按"前端校验"设计。方案本身写得工工整整,逻辑自洽。
错的不是方案,是它读的那份记忆。过期的地图比没有地图更危险:没有地图,你会去翻代码;过期的地图让你停止翻代码——你以为自己已经知道了。
sync-overview 存在的理由,就是让这种情况没有机会累积:每个功能交付之后,记忆必须和现实对一次账。
这一份 skill 是什么
skill 原文对 overview 的定义是这样的:概览是项目的长期记忆——产品能做什么、系统怎么搭起来的,粒度是新人五分钟能读完。每个规划者在澄清之前都读它;只有这一份 skill 写它。
具体是两个文件,放在 .harness/overview/ 下:
product.md——能力域、用户与角色、范围边界、待定方向;technical.md——模块及其边界、数据来源、横切约束、架构方向。
触发时机钉得很死,只有两种:
- 某个功能到达了
done(harness report <feature>显示 phase 为 done),在开下一个功能之前由规划者执行; - 某次 replan 改变了模块边界或能力域。
反过来,澄清和成文期间绝不许用它。那两个阶段只能登记,不能落笔——这是设计一,要展开讲。
它干的事一句话说完:把刚交付的这个功能带来的、已经稳定下来的产品与技术变化,折进上面两份文件,让下一个功能的澄清从当前的事实出发,而不是从一份过期的快照出发。
它怎么干:五步走完一次同步
流程在 skill 里写成五步,没有一步是自由发挥:
- 读齐材料:两份 overview 文件,加上这个功能的
prd.md和tech-design.md(专读其中的「概览增量」一节),再加最终评估报告。 - 列变化清单:新增、调整、废弃,产品侧和技术侧各列一遍。
- 逐条落笔:每条变化写成一短行,放进对应小节。已有的行能覆盖就合并,不在后面追加新行。
- 没有稳定变化时,加一行带日期的记录:“no overview change(<功能名>)"。原文紧跟着一句:不要为了显得有事做而编造一条——never invent one to look busy。
- 还没定下来的事,可以写成"讨论中的方向”(direction under discussion),绝不许写成事实。
交给主 Agent 的产出也有定式:3 到 10 行变化、模块边界是否变了(yes/no)、来自哪个功能。
完成标准一句话:两份文件都已更新(或者留了带日期的"无变化"行),且只有概览层的内容。
还有一张"绝不许写"清单,钉得和流程一样死:
- 任务 id、用例 id、rubric id、提交 SHA;
- 命令、请求/响应正文、逐文件改动清单;
- PRD 或技术方案章节的副本。
这些东西不是没有用,是各有各的家:id 和 SHA 住在状态文件和评测记录里,逐文件清单住在提交历史里。概览只留形状,不留地址。
设计一:增量在澄清时登记,在交付时才落笔
这份 skill 最核心的一个安排,是把"发现变化"和"写进记忆"拆成了两个时刻。
澄清阶段写 prd.md 和 tech-design.md 时,每份文件里都有一节叫「概览增量」:这个功能如果按方案做成,会给 overview 带来哪些变化,先登记在那里。sync-overview 第一步读的就是这两节——它不用自己去代码里考古,账本在设计时就记好了。
但登记只是登记,那个时刻一条都不许写进 overview。为什么?因为澄清时的变化还是意图:功能可能在评测里被打回、被 replan 改道、被砍掉一半,也可能最后落地的形状和设计时不一样。把意图写进长期记忆,等于让计划冒充事实——而记忆的全部价值就在于它只说事实。
等功能真正到达 done,最终评估报告说清了实际交付的是什么,这时才结算:登记过的增量和实际结果对一遍,落定的写进去,没落定的丢掉。
河流里漂着的东西很多,只有沉下来的才成为河床。overview 是河床,不是水流。
设计二:读者很多,写者只有一个
读 overview 的人很多:每个功能的规划者开工前都读。写它的入口只有一个:这份 skill,而且只在交付边界上运行。
显而易见的替代方案是"谁改了什么,谁顺手把 overview 更新了"。这个方案坏在两个地方。
第一,半成品状态。一个功能做到一半时,系统处于"改了一些模块、还没改另一些"的中间态。这时把状态折进 overview,写下的是一套在任何时刻都不曾真实存在过的系统。
第二,互相覆盖。两个功能同时在飞,都碰了同一条模块边界,各自把自己那一半写进记忆,overview 就成了两份意图的拼贴,谁也不是事实。
一个写者、一个时刻——交付边界,功能最终形状已经成为记录在案的事实的时刻——保证 overview 里的每一行都能追溯到某件真正交付过的东西。被 paused 的功能、被废弃的方案,一行都不会留下。
设计三:合并而不是追加,摘要而不是细节
overview 的价值一半在它的小:五分钟读完。按追加的方式写,几个功能下来它就膨胀成一份变更日志——而变更日志恰恰是澄清时期的规划者用不了的东西,它需要的是"现在是什么样",不是"先后发生过什么"。
所以第三步的规则是合并优先:新变化能并进已有的行,就改那一行;只有真正新增的能力域或模块,才开新行。记忆保持恒定的高度,内容随现实更新。
“绝不许写"清单管的是同一件事的另一面:海拔。id、SHA、命令、报文、逐文件清单,都是细节层的记录;把它们抄进概览,概览就从地图降格成档案索引,读的人得先学会查档,才能开始读图。概览回答"有什么、边界在哪”,细节永远留在契约文件和代码里,概览一个字都不复制。
两个小规则:没变化也要留名,未定之事不许写成事实
第四步和第五步看起来是边角,其实堵的是记忆最常见的两种腐坏。
一种是沉默的含糊。如果没变化就什么都不写,下一个读的人分不清"这个功能同步过了,确实没变化"和"这个功能压根没人同步"。带日期的一行"无概览变化",把"没事发生"从缺省值变成一条记录在案的决定——顺带也堵死了为凑数而编造变化的路:既然"无变化"可以正大光明地写,就没有理由编一条假的。
另一种是讨论变事实。架构方向还没拍板,先写进 overview “以示记录”——下一个规划者读到时,标签在转述中丢失,“讨论中的方向"就被当成既定架构用了。所以未定的事可以写,但必须带着标签写:direction under discussion,讨论中的方向,仅此而已。
它的边界
把边界说清。
第一,它只能让地图保持最新,不能让地图变好。如果 overview 最初就画错了——模块边界划得不对、能力域分得别扭——sync-overview 会忠实地把一份坏地图保持到最新。重画地图是架构决策,是人的事,不是同步步骤的事。
第二,它的输入只有账本里有的东西。变化来源是设计时登记的增量加最终评估报告;一处改动如果谁都没登记、评估报告也没提,它不会自己去代码里巡查发现。这份 skill 是对账的,不是审计的。
第三,摘要的海拔是故意的信息损失。“通知收进消息中心"能写成一行,消息中心内部怎么工作写不成。overview 省掉的是考古——告诉你该去读哪些代码和契约;它省不掉阅读本身。
到这里,实现、评测、交付这一组 skill 就拆完了。这个系列还剩最后一组:不绑定任何阶段的通用 skill——下一篇从 plan-before-code 开始,讲为什么"先写计划再写代码"在通用场景里同样是一份写成文件的 skill,而不是一句口头提醒。