DeepSeek Harness 的事故复盘制度:他们把哪些事写成了案卷
翻别人家仓库最难受的一件事,是想知道「这个反直觉的设计当初是被什么坑出来的」,结果文档里只剩结论。deepseek-harness 这个仓库把这件事做成了制度:全站文档明令禁止讲故事,只留一个目录专门收故事。
先说明限定,后面提到的命令、脚本名和路径都受它约束——这个仓库的 README 有一节就叫 Developer preview,原文写着它正在快速迭代,并用大写强调「THERE WILL BE COMPATIBILITY-BREAKING CHANGES」。我们对着的是版本 0.1.0-rc.5 的快照,下面每一处文件名和口径都可能在你读到时已经变了。
先看它把「讲故事」判给了谁
docs/AGENTS.md 里有一张分层表,三列:Tier、Job、Does NOT belong there。绝大多数行第三列都塞得满满的——architecture.md 那行禁止写类型定义和实现状态标注,Agent Notes 那行禁止写迁移计划和验收清单。只有 postmortem/ 那一行的第三列是一个 —,Job 写的是「Incident stories — the only tier where war-story narrative belongs」。全表唯一一行没有禁区。
配套的写作规则在同一份文件里:durable prose 不许出现 previously / now / no longer 这类叙述性措辞,也不许出现 PR 号和 commit;变更故事请放进 commits、PRs、Agent Notes 或 postmortems,并特意补了一句——后两者可以引用已合并的 PR 与 issue 作为证据。再往下是一行归口:bugs → postmortems;rationale → Agent Notes。
这两条合起来解释了一件你翻仓库时会看到的怪现象:docs/postmortem/0001-acp-default-export-drops-inject.md 的状态行敢写「Status: resolved (fix in PR #41 feat/acp-2-bridge)」,而同一个仓库的子系统文档里连一个 PR 号都找不到。不是风格不统一,是分层表给了这一层豁免。
什么样的 bug 才配开一份案卷
docs/postmortem/README.md 第一句就把范围收死:事故复盘记录的是一个 bug 出现在了不该出现的地方——真实用户、已合并的 PR、已发布的版本——值得写的是为什么流程放过了它,而不只是那一行修复。
准入是三条件并列,缺一条就不写:隐蔽(机制不显而易见,细心的工程师也得费力重新推导)、系统性(逃逸原因是测试、工具、约定的缺口,而不是一次性笔误)、重新发现代价高(它消耗了真实调试时间,下次还会)。最后要求链接这份案卷所推动建立的防护措施——测试、AGENTS.md 规则、ADR。
同一份 README 还专门划清了和 Agent Note 的界线:Agent Note 记录一个经过深思熟虑的设计决策及其被否决的替代方案,或者提出未来工作;事故复盘是回顾性的失败记录。这条界线在实操里的意思是,「我们决定以后都这么干」不该写进案卷,「我们那次为什么没拦住」不该写进 Agent Note。
目录里实际有什么
docs/postmortem/ 下我们数出 15 个文件:4 份案卷 0001 到 0004,每份三个文件;加上 README 自己的三个文件。三文件一组是这个仓库的双语契约——X.md、X.zh.md、X.i18n.yaml。
那份 .i18n.yaml 值得单独看一眼,它是整套制度里最机械的一环。以 README.i18n.yaml 为例,正文只有两行,分别是英文侧和中文侧在「上次确认一致」状态下的 git blob hash;文件头的注释写明两种语言拥有同等权威,改完任意一侧要把另一侧带上,并给出重录命令:
pnpm run verify-translation-pairing --write docs/postmortem/README.md
这条命令在根 package.json 的 scripts 里有对应项 verify-translation-pairing,指向 scripts/verify-translation-pairing.ts。也就是说案卷不是随手写完就算,它和其它文档一样进同一道翻译配对门禁。
案卷正文的骨架,README 的说法是:每篇以一段执行摘要开头,一个短段落让忙碌的读者三十秒吸收要点,然后才是「概述、时间线、根因、防护措施」各节。实读四篇,实际节序是:摘要 / 概述 / 影响 / 时间线 / 根因 / 已添加的防护措施 / 教训——「影响」和「教训」两节 README 的那句列举里没提,但四篇都有。0001 还比其它三篇多一节,标题直接叫「为什么所有测试都没有捕获(真正的失败)」,并且把根因拆成了 #1 和 #2 两节。两处口径不一致,以我们实读的四份文件为准。另有一处更小的:0004 的中文侧状态行是英文的 Status: resolved,另外三篇中文侧写的是「状态:已解决」。陈述到此为止。
四篇的共性:时间线是证据,不是叙事
这是最值得抄走的一条。.agents/skills/dsh-doc-standards/SKILL.md 里把案卷定义为「以一起事故为范围的 reference」,要求保留其必需的时序证据,但不要把时序当成教学顺序。
0003 把这条执行得最狠。那篇讲的是一个 Web agent 改完 GUI 源码,跑去验收另一个端口上的替代服务器。它的时间线不是回忆录,锚点是持久化事件日志里的序列号:初始请求头在序列 6,面向用户的交接在 30939,裸 Vite 启动在 31865,替代宿主启动在 34309,启动 manifest 探测在 34441,首次探测原端口进程在 34681。写完锚点,它加了一句我认为是整个目录里最有价值的方法论——下方时间线以这些事件为依据,而不是根据后续报告反推意图。
你自己写复盘时最容易翻车的就是这一步:事后回忆天然会把「当时该想到的」写成「当时想到了」。锚一个事后能回日志里查证的序号,比锚记忆可靠。
共性之二:必须回答「每道安全网为什么都没拦住」
四篇都有一节在算这笔账,而且算得不留情面。
0001 里的原文是:尽管有 178 个绿色单元测试和 100% 行覆盖率,bridge 在生产环境中完全无法工作(这两个数字是案卷自述,我们没有跑过它的测试)。它给出的解释不是「测试写得不够多」,而是所有测试都通过一条不触及插件真实加载方式的路径挂载插件——手动构造插件对象把 inject 直接喂了进去,而丢掉 inject 的那段逻辑只有 Loader 才会调用。收尾那句可以直接贴在会议室墙上:覆盖率证明代码行被执行过,不能说明功能按交付方式正常工作。
0002 是另一个味道。文件系统快照工具被一个字面量 !!js 对象永久禁用,快照套件却全绿——因为刷新后的预期输出把 UNKNOWN_TOOL 的失败结果接受成了新基线。案卷给出的教训是:快照刷新是 fixture 的生产过程,不是正确性审查;诸如已注册工具缺失这类语义上不可能的结果,需要独立于预期输出的断言。
0004 讲的是 Landlock 在较旧 ABI 上打印的那条无害「部分强制执行」通知,被 harness 与任意非零子进程退出组合成了 launcher 失败,于是 ripgrep 无匹配时的退出码 1 被呈现为 SANDBOX_UNAVAILABLE。这篇必须原样转述它自己的限定:案卷明写 stderr 仍是带内归因通道,受限子进程可以故意复现 runner 的门控致命诊断行与退出状态,造成可用性或诊断误归因;带外状态协议属于独立的加固工作,而不是一个沙箱绕过修复。案卷自己都不肯说「修完就安全了」,我们更不该替它说。
共性之三:防护措施必须落到能在树里翻到的东西上
「以后注意」在这套模板里不算防护措施。0004 的防护措施节列了六条,每条都挂着具体产物:分类规则、dsh-sandbox-local 与 dsh-bash-sandbox 两个包、走 ctx.subprocess 跑打包 ripgrep 的 dsh-tool-fs-search、原生边界回归用例 packages/shell/bash-sandbox/tests/partial-landlock.spec.ts,以及固定组装后产品路径的 examples/acp-agent/partial-landlock.cordis.snapshot.yml。我们逐个去树里找过,这几处路径当前都在。0002 列的 verify-cordis-config 也确实存在:scripts/verify-cordis-config.ts 在树里,根 package.json 的 hygiene 串里挂着 pnpm run verify-cordis-config 这一环。
读案卷时这一节的正确用法是当索引:想验证某条防护是否还活着,照着路径去树里翻,比读结论快。
有一条防护措施的表述已经和今天的源码对不上
0002 的防护措施节写着,AGENTS.md 与 Cordis 入门明确说明 !!js 仅在插件 config 内有效,条件式组合应使用 overlay。
而今天根目录 AGENTS.md 的对应一行写的是 cordis.yml 允许 !!js(never !js)出现在插件 config 和条目 disabled 下,其它元数据保持字面值。scripts/verify-cordis-config.ts 的文件头注释是同一口径:Loader 会插值插件的 config 与条目的 disabled 字段,其余元数据字段保持静态;脚本正文里也确实为 disabled 单开了一条分支,允许它自身携带表达式节点并做 parse 校验。
这个变化在 .agents/notes/implemented/architecture/2026-08-11-loader-entry-disabled-interpolation.md 有记录,那份 Agent Note 自述:disabled 是唯一被插值的元数据字段,其它字段的表达式门禁继续拒绝,disabled 上的 postmortem-0002 隐患以「求值」而非「禁止」关闭。
三处文本放在一起就是:案卷记的是事故当时新增的防护,Agent Note 记的是后来改的决策,源码是当下的事实。以我们实读的源码为准。这也提醒一件事——案卷是回顾性记录,它不是当前配置手册,别拿一份 0002 去指导今天怎么写 cordis.yml。
抄得走的部分
不用等你的项目也长到这个体量。真正可移植的是三样:一是准入三条件,隐蔽 + 系统性 + 重新发现代价高,同时满足才立案,否则复盘会退化成周报;二是时间线锚可验证的事件标识而不是记忆;三是防护措施一律写成能在仓库里翻到的路径,写不出路径就说明那条其实还没落地。
至于「为什么我们的流程放过了它」这句问法本身——deepseek-harness 把它放在 docs/postmortem/README.md 的第一句,位置就说明了权重。
本文依据 DeepSeek Harness 官方仓库(github.com/deepseek-ai/deepseek-harness)的 README、docs/ 下的
架构与子系统文档、以及 packages/ 下的源码整理,核对日 2026-08-17,对应仓库快照 47f9438(版本 0.1.0-rc.5)。
本文内容为仓库源码与文档口径,我们没有安装、也没有运行过这个项目,
因此不涉及界面外观、操作手感与运行速度的任何描述。
该仓库 README 自述处于开发者预览阶段并明确说明未来会有破坏兼容性的变更,
文中出现的命令、配置与默认值随时可能变动,请以仓库最新内容为准。
安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。