【Agent 架构实战】大模型长期项目开发:决策文档生命周期管理与 CI 门禁治理
如果你也在用 Agent 维护长期项目,大概率撞过这样的墙:上个季度就废弃的刷新策略,Agent 偏偏检索到旧文档,理直气壮地写回代码;你问它为什么,它还贴出"出处"证明自己有据可查。问题不在 Agent 记性差,而在仓库从未用机器可读的方式表达过"哪些文档还算数"。本文给出一套零依赖、可直接抄走的方案:三层知识解耦、四状态决策目录、一条 SHA-256 哈希门禁。
适合谁 :用 Claude Code / Codex / Cursor 等 Agent 参与数月以上周期项目开发的工程师
解决什么 :废弃文档被 Agent 误检索导致的逻辑冲突、状态漂移与历史改写
读完得到:一套"目录即状态机"的 notes 结构 + 可直接运行的门禁脚本 + 存量项目 6 步迁移路径
文章目录
- [【Agent 架构实战】大模型长期项目开发:决策文档生命周期管理与 CI 门禁治理](#【Agent 架构实战】大模型长期项目开发:决策文档生命周期管理与 CI 门禁治理)
-
- 太长不看:五分钟最小方案
- [一、 核心痛点:为什么长期 Agent 项目的文档容易"腐烂"?](#一、 核心痛点:为什么长期 Agent 项目的文档容易"腐烂"?)
-
- [【图解 1:Agent 上下文知识三层分层架构】](#【图解 1:Agent 上下文知识三层分层架构】)
- 资料职责划分与维护原则
- [二、 决策记录判定标准:什么内容值得写?](#二、 决策记录判定标准:什么内容值得写?)
-
- [【图解 2:价值三问判定流程】](#【图解 2:价值三问判定流程】)
- [值得记录 vs 避免记录的范围对比](#值得记录 vs 避免记录的范围对比)
- [三、 ADR 状态机模型与生命周期流转](#三、 ADR 状态机模型与生命周期流转)
-
- [记录格式:Front-matter 最小规范](#记录格式:Front-matter 最小规范)
- [【图解 3:决策文档状态转换流转】](#【图解 3:决策文档状态转换流转】)
- 状态转换三大核心避坑守则
- [四、 部分取代与完全取代的精准处理机制](#四、 部分取代与完全取代的精准处理机制)
-
- [【图解 4:文档更新处理分支】](#【图解 4:文档更新处理分支】)
- [五、 防篡改机制:校验归档原文与 SHA-256 摘要](#五、 防篡改机制:校验归档原文与 SHA-256 摘要)
-
- [【图解 5:归档校验与封存流程】](#【图解 5:归档校验与封存流程】)
- 完整实现:scripts/verify-docs.mjs
- [六、 Agent 协同开发工作流与门禁集成](#六、 Agent 协同开发工作流与门禁集成)
-
- 任务生命周期与执行动作映射表
- [Git Hook 与 CI 集成](#Git Hook 与 CI 集成)
- [七、 存量项目治理落地方案(6 步迁移指南)](#七、 存量项目治理落地方案(6 步迁移指南))
- [八、 借鉴 DeepSeek Harness (dsh) 的治理启示](#八、 借鉴 DeepSeek Harness (dsh) 的治理启示)
-
- [为什么不直接用 MADR / adr-tools](#为什么不直接用 MADR / adr-tools)
- 总结
太长不看:五分钟最小方案
不想一次吃下整套体系?先做这三件事,就能挡住大部分 Agent 误读:
- 建目录 :
.agents/notes/下建proposed/、implemented/、rejected/、archived/四个空目录; - 抢救高危记录 :把手里最容易被 Agent 误读的 3 个旧决策 写成
implemented/记录(front-matter 只要id/status/date三个字段,格式见第三节); - 改一行规则 :在
AGENTS.md里加一句硬规则------"中等以上任务开工前,先按领域关键词检索.agents/notes/下的proposed/、implemented/、rejected/三个目录"。
到这一步没有任何脚本和 CI,纯约定就能解决八成误读。后文的 SHA-256 哈希门禁是给多人协作、高频提交团队的加固项,可以等痛点真的出现再上。
一、 核心痛点:为什么长期 Agent 项目的文档容易"腐烂"?
当一个项目持续由 AI Agent 参与开发数月后,代码库内通常会积累大量的设计说明、实施计划与决策记录。最初这些文档提供了宝贵的上下文;但随着业务迭代,它们往往会变成新的问题源头:
- 事实与历史混淆:代码已采用新方案,旧文档仍写着原方案有效。
- 状态漂移:提案早已落地,文件却停留在待评审(Draft/Proposed)目录。
- 碎片化与覆盖冲突:同一个问题先后写了三份记录,各自保留不同的细节,Agent 搜索到哪一份便做出不同判断。
为了彻底解决上下文污染,必须在架构层面实现知识分层解耦。
【图解 1:Agent 上下文知识三层分层架构】
#mermaid-svg-MAI7DJ4ZdzDAHBGh{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-MAI7DJ4ZdzDAHBGh .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-MAI7DJ4ZdzDAHBGh .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-MAI7DJ4ZdzDAHBGh .error-icon{fill:#552222;}#mermaid-svg-MAI7DJ4ZdzDAHBGh .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-MAI7DJ4ZdzDAHBGh .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-MAI7DJ4ZdzDAHBGh .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-MAI7DJ4ZdzDAHBGh .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-MAI7DJ4ZdzDAHBGh .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-MAI7DJ4ZdzDAHBGh .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-MAI7DJ4ZdzDAHBGh .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-MAI7DJ4ZdzDAHBGh .marker{fill:#333333;stroke:#333333;}#mermaid-svg-MAI7DJ4ZdzDAHBGh .marker.cross{stroke:#333333;}#mermaid-svg-MAI7DJ4ZdzDAHBGh svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-MAI7DJ4ZdzDAHBGh p{margin:0;}#mermaid-svg-MAI7DJ4ZdzDAHBGh .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-MAI7DJ4ZdzDAHBGh .cluster-label text{fill:#333;}#mermaid-svg-MAI7DJ4ZdzDAHBGh .cluster-label span{color:#333;}#mermaid-svg-MAI7DJ4ZdzDAHBGh .cluster-label span p{background-color:transparent;}#mermaid-svg-MAI7DJ4ZdzDAHBGh .label text,#mermaid-svg-MAI7DJ4ZdzDAHBGh span{fill:#333;color:#333;}#mermaid-svg-MAI7DJ4ZdzDAHBGh .node rect,#mermaid-svg-MAI7DJ4ZdzDAHBGh .node circle,#mermaid-svg-MAI7DJ4ZdzDAHBGh .node ellipse,#mermaid-svg-MAI7DJ4ZdzDAHBGh .node polygon,#mermaid-svg-MAI7DJ4ZdzDAHBGh .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-MAI7DJ4ZdzDAHBGh .rough-node .label text,#mermaid-svg-MAI7DJ4ZdzDAHBGh .node .label text,#mermaid-svg-MAI7DJ4ZdzDAHBGh .image-shape .label,#mermaid-svg-MAI7DJ4ZdzDAHBGh .icon-shape .label{text-anchor:middle;}#mermaid-svg-MAI7DJ4ZdzDAHBGh .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-MAI7DJ4ZdzDAHBGh .rough-node .label,#mermaid-svg-MAI7DJ4ZdzDAHBGh .node .label,#mermaid-svg-MAI7DJ4ZdzDAHBGh .image-shape .label,#mermaid-svg-MAI7DJ4ZdzDAHBGh .icon-shape .label{text-align:center;}#mermaid-svg-MAI7DJ4ZdzDAHBGh .node.clickable{cursor:pointer;}#mermaid-svg-MAI7DJ4ZdzDAHBGh .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-MAI7DJ4ZdzDAHBGh .arrowheadPath{fill:#333333;}#mermaid-svg-MAI7DJ4ZdzDAHBGh .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-MAI7DJ4ZdzDAHBGh .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-MAI7DJ4ZdzDAHBGh .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-MAI7DJ4ZdzDAHBGh .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-MAI7DJ4ZdzDAHBGh .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-MAI7DJ4ZdzDAHBGh .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-MAI7DJ4ZdzDAHBGh .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-MAI7DJ4ZdzDAHBGh .cluster text{fill:#333;}#mermaid-svg-MAI7DJ4ZdzDAHBGh .cluster span{color:#333;}#mermaid-svg-MAI7DJ4ZdzDAHBGh div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-MAI7DJ4ZdzDAHBGh .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-MAI7DJ4ZdzDAHBGh rect.text{fill:none;stroke-width:0;}#mermaid-svg-MAI7DJ4ZdzDAHBGh .icon-shape,#mermaid-svg-MAI7DJ4ZdzDAHBGh .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-MAI7DJ4ZdzDAHBGh .icon-shape p,#mermaid-svg-MAI7DJ4ZdzDAHBGh .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-MAI7DJ4ZdzDAHBGh .icon-shape .label rect,#mermaid-svg-MAI7DJ4ZdzDAHBGh .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-MAI7DJ4ZdzDAHBGh .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-MAI7DJ4ZdzDAHBGh .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-MAI7DJ4ZdzDAHBGh :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Layer 3 · 决策依据层(ADR Notes)
.agents/notes/ 四状态目录
回答:为什么这样设计?放弃了什么?何时重评?
Layer 2 · 行为规范层(Execution Rules)
AGENTS.md / .cursorrules / 指令规则
回答:开发与执行必须遵守哪些硬性规则?
Layer 1 · 代码事实层(Code Facts)
源码 / 配置 / 数据库 Migration / 单元测试
回答:系统当前的真实行为是什么?
资料职责划分与维护原则
| 资料类型 | 主要回答什么问题 | 维护与更新原则 |
|---|---|---|
| 代码/配置/迁移/测试 | 当前实现是什么?已有证据覆盖到哪里? | 随实现实时更新,运行环境需实际验证。 |
| AGENTS.md | 开发和执行必须遵守哪些规则? | 保持明确、极简、可执行,链接详细依据。 |
| 决策记录 (Notes/ADR) | 为什么选择这个方案?放弃了什么?何时重新评估? | 严格管理状态、有效范围与取代关系。 |
| 当前接口/业务文档 | 使用者现在应依赖什么契约? | 随行为变化更新,避免多处复制同一事实。 |
| 实施计划/快照 | 当时准备怎么做?当时发生了什么? | 保留特定时点范围,不自动当作当前执行授权。 |
关键原则:代码可以证明实际行为,
AGENTS.md定义允许的行为。不能用"代码就是这样写的"作为继续扩大违规的理由,也不能把实施计划里的"下一步操作"直接误读为当前执行授权。
二、 决策记录判定标准:什么内容值得写?
为了避免文档泛滥,在写决策记录前先过价值三问------按顺序短路判定,问出结果即停:
【图解 2:价值三问判定流程】
#mermaid-svg-BRJxXZIiKmgsx2gO{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-BRJxXZIiKmgsx2gO .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-BRJxXZIiKmgsx2gO .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-BRJxXZIiKmgsx2gO .error-icon{fill:#552222;}#mermaid-svg-BRJxXZIiKmgsx2gO .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-BRJxXZIiKmgsx2gO .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-BRJxXZIiKmgsx2gO .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-BRJxXZIiKmgsx2gO .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-BRJxXZIiKmgsx2gO .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-BRJxXZIiKmgsx2gO .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-BRJxXZIiKmgsx2gO .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-BRJxXZIiKmgsx2gO .marker{fill:#333333;stroke:#333333;}#mermaid-svg-BRJxXZIiKmgsx2gO .marker.cross{stroke:#333333;}#mermaid-svg-BRJxXZIiKmgsx2gO svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-BRJxXZIiKmgsx2gO p{margin:0;}#mermaid-svg-BRJxXZIiKmgsx2gO .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-BRJxXZIiKmgsx2gO .cluster-label text{fill:#333;}#mermaid-svg-BRJxXZIiKmgsx2gO .cluster-label span{color:#333;}#mermaid-svg-BRJxXZIiKmgsx2gO .cluster-label span p{background-color:transparent;}#mermaid-svg-BRJxXZIiKmgsx2gO .label text,#mermaid-svg-BRJxXZIiKmgsx2gO span{fill:#333;color:#333;}#mermaid-svg-BRJxXZIiKmgsx2gO .node rect,#mermaid-svg-BRJxXZIiKmgsx2gO .node circle,#mermaid-svg-BRJxXZIiKmgsx2gO .node ellipse,#mermaid-svg-BRJxXZIiKmgsx2gO .node polygon,#mermaid-svg-BRJxXZIiKmgsx2gO .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-BRJxXZIiKmgsx2gO .rough-node .label text,#mermaid-svg-BRJxXZIiKmgsx2gO .node .label text,#mermaid-svg-BRJxXZIiKmgsx2gO .image-shape .label,#mermaid-svg-BRJxXZIiKmgsx2gO .icon-shape .label{text-anchor:middle;}#mermaid-svg-BRJxXZIiKmgsx2gO .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-BRJxXZIiKmgsx2gO .rough-node .label,#mermaid-svg-BRJxXZIiKmgsx2gO .node .label,#mermaid-svg-BRJxXZIiKmgsx2gO .image-shape .label,#mermaid-svg-BRJxXZIiKmgsx2gO .icon-shape .label{text-align:center;}#mermaid-svg-BRJxXZIiKmgsx2gO .node.clickable{cursor:pointer;}#mermaid-svg-BRJxXZIiKmgsx2gO .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-BRJxXZIiKmgsx2gO .arrowheadPath{fill:#333333;}#mermaid-svg-BRJxXZIiKmgsx2gO .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-BRJxXZIiKmgsx2gO .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-BRJxXZIiKmgsx2gO .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-BRJxXZIiKmgsx2gO .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-BRJxXZIiKmgsx2gO .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-BRJxXZIiKmgsx2gO .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-BRJxXZIiKmgsx2gO .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-BRJxXZIiKmgsx2gO .cluster text{fill:#333;}#mermaid-svg-BRJxXZIiKmgsx2gO .cluster span{color:#333;}#mermaid-svg-BRJxXZIiKmgsx2gO div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-BRJxXZIiKmgsx2gO .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-BRJxXZIiKmgsx2gO rect.text{fill:none;stroke-width:0;}#mermaid-svg-BRJxXZIiKmgsx2gO .icon-shape,#mermaid-svg-BRJxXZIiKmgsx2gO .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-BRJxXZIiKmgsx2gO .icon-shape p,#mermaid-svg-BRJxXZIiKmgsx2gO .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-BRJxXZIiKmgsx2gO .icon-shape .label rect,#mermaid-svg-BRJxXZIiKmgsx2gO .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-BRJxXZIiKmgsx2gO .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-BRJxXZIiKmgsx2gO .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-BRJxXZIiKmgsx2gO :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
否
是
是
否
新设计 / 变更想法
Q1:代码、测试与现有文档
已能完整表达该内容?
不单独建记录
Q2:将来是否可能
重新讨论这个选择?
由提交记录表达即可
Q3:丢失这段理由,是否可能
恢复出有问题的设计?
必须建立决策记录
值得记录 vs 避免记录的范围对比
- 必须记录的内容 :
- 模块边界与数据所有权(如哪些服务可写入特定数据)。
- 时间、协议与持久化语义(如审计时间与业务日期的不同口径)。
- 安全或兼容性保证(如生产写入必须经过明确保护的原因)。
- 存在实际备选方案的选择,以及接受某项成本/代价的原因。
- 方案被否决后,未来仍有人可能再次提出该方案的理由。
- 应避免建库的内容 :
- 局部配色、机械性代码重命名、普通的重构清理。
- 已经完成的常规操作清单。
三、 ADR 状态机模型与生命周期流转
在仓库中建立形如 .agents/notes/ 的状态生命周期结构:
text
.agents/notes/
├── README.md
├── proposed/ # 尚未落地的提案
├── implemented/ # 已落地、仍指导开发的决策
├── rejected/ # 已被明确否决的方案
├── archived/ # 已完成、仅存历史参考价值的归档("记忆冰室":只进不改)
└── archive-manifest.json # 归档哈希校验清单(第五节生成)
记录格式:Front-matter 最小规范
状态不能只靠目录表达,每条记录头部用 YAML 声明身份,门禁脚本才能机器校验:
yaml
---
id: note-0007
title: 广告报告采集窗口由 7 天调整为 14 天
status: implemented # proposed | implemented | rejected | archived
supersedes: note-0002 # 可选:被本记录完全/部分取代的旧记录 id
date: 2026-04-01
---
三条硬规则:
status必须与所在目录名一致(门禁据此拦截"文件放错状态");supersedes指向的id必须真实存在;被完全取代的记录移入archived/,被部分取代的留在原目录并缩小有效范围(见第四节);id全局唯一,建议note-XXXX递增。
【图解 3:决策文档状态转换流转】
#mermaid-svg-hLrGvtoVcHWogrBn{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-hLrGvtoVcHWogrBn .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-hLrGvtoVcHWogrBn .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-hLrGvtoVcHWogrBn .error-icon{fill:#552222;}#mermaid-svg-hLrGvtoVcHWogrBn .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-hLrGvtoVcHWogrBn .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-hLrGvtoVcHWogrBn .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-hLrGvtoVcHWogrBn .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-hLrGvtoVcHWogrBn .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-hLrGvtoVcHWogrBn .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-hLrGvtoVcHWogrBn .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-hLrGvtoVcHWogrBn .marker{fill:#333333;stroke:#333333;}#mermaid-svg-hLrGvtoVcHWogrBn .marker.cross{stroke:#333333;}#mermaid-svg-hLrGvtoVcHWogrBn svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-hLrGvtoVcHWogrBn p{margin:0;}#mermaid-svg-hLrGvtoVcHWogrBn defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-hLrGvtoVcHWogrBn g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-hLrGvtoVcHWogrBn g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-hLrGvtoVcHWogrBn g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-hLrGvtoVcHWogrBn g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-hLrGvtoVcHWogrBn g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-hLrGvtoVcHWogrBn .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-hLrGvtoVcHWogrBn .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-hLrGvtoVcHWogrBn .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-hLrGvtoVcHWogrBn .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-hLrGvtoVcHWogrBn .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-hLrGvtoVcHWogrBn .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-hLrGvtoVcHWogrBn .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-hLrGvtoVcHWogrBn .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-hLrGvtoVcHWogrBn .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-hLrGvtoVcHWogrBn .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-hLrGvtoVcHWogrBn .edgeLabel .label text{fill:#333;}#mermaid-svg-hLrGvtoVcHWogrBn .label div .edgeLabel{color:#333;}#mermaid-svg-hLrGvtoVcHWogrBn .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-hLrGvtoVcHWogrBn .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-hLrGvtoVcHWogrBn .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-hLrGvtoVcHWogrBn .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-hLrGvtoVcHWogrBn .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-hLrGvtoVcHWogrBn .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-hLrGvtoVcHWogrBn .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-hLrGvtoVcHWogrBn #statediagram-barbEnd{fill:#333333;}#mermaid-svg-hLrGvtoVcHWogrBn .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-hLrGvtoVcHWogrBn .cluster-label,#mermaid-svg-hLrGvtoVcHWogrBn .nodeLabel{color:#131300;}#mermaid-svg-hLrGvtoVcHWogrBn .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-hLrGvtoVcHWogrBn .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-hLrGvtoVcHWogrBn .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-hLrGvtoVcHWogrBn .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-hLrGvtoVcHWogrBn .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-hLrGvtoVcHWogrBn .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-hLrGvtoVcHWogrBn .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-hLrGvtoVcHWogrBn .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-hLrGvtoVcHWogrBn .note-edge{stroke-dasharray:5;}#mermaid-svg-hLrGvtoVcHWogrBn .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-hLrGvtoVcHWogrBn .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-hLrGvtoVcHWogrBn .statediagram-note text{fill:black;}#mermaid-svg-hLrGvtoVcHWogrBn .statediagram-note .nodeLabel{color:black;}#mermaid-svg-hLrGvtoVcHWogrBn .statediagram .edgeLabel{color:red;}#mermaid-svg-hLrGvtoVcHWogrBn #dependencyStart,#mermaid-svg-hLrGvtoVcHWogrBn #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-hLrGvtoVcHWogrBn .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-hLrGvtoVcHWogrBn :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 新决策提案
落地实施并补充证据
明确否决并记录原因
仅剩历史追溯价值 / 被完全取代
proposed
implemented
rejected
archived
部分取代:新记录声明 supersedes
旧记录缩小有效范围后留在本状态
所谓"记忆冰室":
archived/只进不改------封存后的正文与路径被哈希锁死(第五节),历史原文不随现状变化,新事实永远写在活跃目录并单向引用历史。
状态转换三大核心避坑守则
- 已完成 ≠ 应当归档 :一个安全边界即便已实施多年,只要它依然指导每次相关修改,就必须留在
implemented/活跃目录。 - 没实施过的提案,归宿是
rejected/而不是archived/:archived/只收"曾经真实实施过、后被取代"的历史;从未落地的想法不产生历史价值。 - 曾经实施 ≠ 标为
rejected:旧方案被新方案取代并不改变它曾运行的事实。正确做法是在新记录 front-matter 里声明supersedes形成取代链,而不是改写旧记录的状态。
四、 部分取代与完全取代的精准处理机制
处理文档更新时,最忌讳直接删除旧文档或简单加上一句"本文已被取代"。
【图解 4:文档更新处理分支】
#mermaid-svg-xIdFGSwavqfwPVBt{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-xIdFGSwavqfwPVBt .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-xIdFGSwavqfwPVBt .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-xIdFGSwavqfwPVBt .error-icon{fill:#552222;}#mermaid-svg-xIdFGSwavqfwPVBt .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-xIdFGSwavqfwPVBt .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-xIdFGSwavqfwPVBt .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-xIdFGSwavqfwPVBt .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-xIdFGSwavqfwPVBt .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-xIdFGSwavqfwPVBt .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-xIdFGSwavqfwPVBt .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-xIdFGSwavqfwPVBt .marker{fill:#333333;stroke:#333333;}#mermaid-svg-xIdFGSwavqfwPVBt .marker.cross{stroke:#333333;}#mermaid-svg-xIdFGSwavqfwPVBt svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-xIdFGSwavqfwPVBt p{margin:0;}#mermaid-svg-xIdFGSwavqfwPVBt .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-xIdFGSwavqfwPVBt .cluster-label text{fill:#333;}#mermaid-svg-xIdFGSwavqfwPVBt .cluster-label span{color:#333;}#mermaid-svg-xIdFGSwavqfwPVBt .cluster-label span p{background-color:transparent;}#mermaid-svg-xIdFGSwavqfwPVBt .label text,#mermaid-svg-xIdFGSwavqfwPVBt span{fill:#333;color:#333;}#mermaid-svg-xIdFGSwavqfwPVBt .node rect,#mermaid-svg-xIdFGSwavqfwPVBt .node circle,#mermaid-svg-xIdFGSwavqfwPVBt .node ellipse,#mermaid-svg-xIdFGSwavqfwPVBt .node polygon,#mermaid-svg-xIdFGSwavqfwPVBt .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-xIdFGSwavqfwPVBt .rough-node .label text,#mermaid-svg-xIdFGSwavqfwPVBt .node .label text,#mermaid-svg-xIdFGSwavqfwPVBt .image-shape .label,#mermaid-svg-xIdFGSwavqfwPVBt .icon-shape .label{text-anchor:middle;}#mermaid-svg-xIdFGSwavqfwPVBt .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-xIdFGSwavqfwPVBt .rough-node .label,#mermaid-svg-xIdFGSwavqfwPVBt .node .label,#mermaid-svg-xIdFGSwavqfwPVBt .image-shape .label,#mermaid-svg-xIdFGSwavqfwPVBt .icon-shape .label{text-align:center;}#mermaid-svg-xIdFGSwavqfwPVBt .node.clickable{cursor:pointer;}#mermaid-svg-xIdFGSwavqfwPVBt .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-xIdFGSwavqfwPVBt .arrowheadPath{fill:#333333;}#mermaid-svg-xIdFGSwavqfwPVBt .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-xIdFGSwavqfwPVBt .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-xIdFGSwavqfwPVBt .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xIdFGSwavqfwPVBt .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-xIdFGSwavqfwPVBt .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xIdFGSwavqfwPVBt .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-xIdFGSwavqfwPVBt .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-xIdFGSwavqfwPVBt .cluster text{fill:#333;}#mermaid-svg-xIdFGSwavqfwPVBt .cluster span{color:#333;}#mermaid-svg-xIdFGSwavqfwPVBt div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-xIdFGSwavqfwPVBt .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-xIdFGSwavqfwPVBt rect.text{fill:none;stroke-width:0;}#mermaid-svg-xIdFGSwavqfwPVBt .icon-shape,#mermaid-svg-xIdFGSwavqfwPVBt .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xIdFGSwavqfwPVBt .icon-shape p,#mermaid-svg-xIdFGSwavqfwPVBt .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-xIdFGSwavqfwPVBt .icon-shape .label rect,#mermaid-svg-xIdFGSwavqfwPVBt .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xIdFGSwavqfwPVBt .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-xIdFGSwavqfwPVBt .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-xIdFGSwavqfwPVBt :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 只替换部分规则
完整承接全部职责
要更新一份决策记录
新决策的覆盖范围?
部分取代(Partial)
完全取代(Complete)
旧记录保留仍有效的条款
双方以 supersedes 互链,声明各自边界
先迁移旧记录独有的动机 / 代价 / 边界
清理入站引用后,旧记录移入 archived/
- 部分取代示例 :ERP 接口调整中,刷新窗口由 7 天改为 14 天。旧方案中"首次建立完整基线"与"站点日期校验"的理由依然有效。此时新记录(
note-0007)在supersedes中声明只取代刷新窗口,旧记录(note-0002)保留基线与幂等约束并注明"窗口规则以 note-0007 为准",双方相互引用。 - 完全取代示例 :新记录完整接管职责前,必须先将旧记录中独有的动机、备选方案、代价和覆盖缺口迁移至新记录,清理完全部入站引用后才能把旧记录移入
archived/并封存。
五、 防篡改机制:校验归档原文与 SHA-256 摘要
为防止归档文件在日后被 Agent 或人误改写,用一个哈希清单锁死归档区:封存时把每份 archived/*.md 的 SHA-256 写入 archive-manifest.json;此后每次校验都以 Git HEAD 中的 manifest 为可信基准------即使有人连工作区的 manifest 一起改掉,与 Git 历史一比也会现形。
【图解 5:归档校验与封存流程】
#mermaid-svg-M3OV0ZPyBSc8PXLf{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-M3OV0ZPyBSc8PXLf .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-M3OV0ZPyBSc8PXLf .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-M3OV0ZPyBSc8PXLf .error-icon{fill:#552222;}#mermaid-svg-M3OV0ZPyBSc8PXLf .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-M3OV0ZPyBSc8PXLf .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-M3OV0ZPyBSc8PXLf .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-M3OV0ZPyBSc8PXLf .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-M3OV0ZPyBSc8PXLf .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-M3OV0ZPyBSc8PXLf .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-M3OV0ZPyBSc8PXLf .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-M3OV0ZPyBSc8PXLf .marker{fill:#333333;stroke:#333333;}#mermaid-svg-M3OV0ZPyBSc8PXLf .marker.cross{stroke:#333333;}#mermaid-svg-M3OV0ZPyBSc8PXLf svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-M3OV0ZPyBSc8PXLf p{margin:0;}#mermaid-svg-M3OV0ZPyBSc8PXLf .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-M3OV0ZPyBSc8PXLf .cluster-label text{fill:#333;}#mermaid-svg-M3OV0ZPyBSc8PXLf .cluster-label span{color:#333;}#mermaid-svg-M3OV0ZPyBSc8PXLf .cluster-label span p{background-color:transparent;}#mermaid-svg-M3OV0ZPyBSc8PXLf .label text,#mermaid-svg-M3OV0ZPyBSc8PXLf span{fill:#333;color:#333;}#mermaid-svg-M3OV0ZPyBSc8PXLf .node rect,#mermaid-svg-M3OV0ZPyBSc8PXLf .node circle,#mermaid-svg-M3OV0ZPyBSc8PXLf .node ellipse,#mermaid-svg-M3OV0ZPyBSc8PXLf .node polygon,#mermaid-svg-M3OV0ZPyBSc8PXLf .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-M3OV0ZPyBSc8PXLf .rough-node .label text,#mermaid-svg-M3OV0ZPyBSc8PXLf .node .label text,#mermaid-svg-M3OV0ZPyBSc8PXLf .image-shape .label,#mermaid-svg-M3OV0ZPyBSc8PXLf .icon-shape .label{text-anchor:middle;}#mermaid-svg-M3OV0ZPyBSc8PXLf .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-M3OV0ZPyBSc8PXLf .rough-node .label,#mermaid-svg-M3OV0ZPyBSc8PXLf .node .label,#mermaid-svg-M3OV0ZPyBSc8PXLf .image-shape .label,#mermaid-svg-M3OV0ZPyBSc8PXLf .icon-shape .label{text-align:center;}#mermaid-svg-M3OV0ZPyBSc8PXLf .node.clickable{cursor:pointer;}#mermaid-svg-M3OV0ZPyBSc8PXLf .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-M3OV0ZPyBSc8PXLf .arrowheadPath{fill:#333333;}#mermaid-svg-M3OV0ZPyBSc8PXLf .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-M3OV0ZPyBSc8PXLf .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-M3OV0ZPyBSc8PXLf .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-M3OV0ZPyBSc8PXLf .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-M3OV0ZPyBSc8PXLf .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-M3OV0ZPyBSc8PXLf .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-M3OV0ZPyBSc8PXLf .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-M3OV0ZPyBSc8PXLf .cluster text{fill:#333;}#mermaid-svg-M3OV0ZPyBSc8PXLf .cluster span{color:#333;}#mermaid-svg-M3OV0ZPyBSc8PXLf div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-M3OV0ZPyBSc8PXLf .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-M3OV0ZPyBSc8PXLf rect.text{fill:none;stroke-width:0;}#mermaid-svg-M3OV0ZPyBSc8PXLf .icon-shape,#mermaid-svg-M3OV0ZPyBSc8PXLf .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-M3OV0ZPyBSc8PXLf .icon-shape p,#mermaid-svg-M3OV0ZPyBSc8PXLf .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-M3OV0ZPyBSc8PXLf .icon-shape .label rect,#mermaid-svg-M3OV0ZPyBSc8PXLf .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-M3OV0ZPyBSc8PXLf .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-M3OV0ZPyBSc8PXLf .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-M3OV0ZPyBSc8PXLf :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 封存:node scripts/verify-docs.mjs --seal-archives
是
否
扫描 archived/ 全部 md
已封存条目
哈希发生变化?
🚨 拒绝:须先解冻并重新评审
仅追加新条目到 manifest
绝不覆盖旧哈希
日常校验:node scripts/verify-docs.mjs
否(首次校验)
是
是
否
读取工作区 archive-manifest.json
Git HEAD 中存在
可信基准 manifest?
跳过哈希基准
仅校验状态目录一致性
逐条比对清单条目
是否被删除或篡改
读取 archived/ 正文
计算 SHA-256 与清单比对
全部一致?
✅ 门禁通过
🚨 阻断提交
完整实现:scripts/verify-docs.mjs
运行环境:Node.js ≥ 18,零依赖,Windows / Linux 均可执行。整个脚本不到 100 行,包含状态目录校验、Git 基准比对、封存三条命令的全部逻辑:
javascript
#!/usr/bin/env node
// scripts/verify-docs.mjs ------ 决策文档门禁:状态目录校验 + 归档哈希冻结
import { createHash } from 'node:crypto';
import { execFileSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import fs from 'node:fs';
import path from 'node:path';
const NOTES = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '.agents/notes');
const MANIFEST = path.join(NOTES, 'archive-manifest.json');
// 统一换行符后计算 SHA-256,兼容 Windows 与 Linux 工作区
const digest = t =>
createHash('sha256').update(t.replaceAll('\r\n', '\n')).digest('hex');
// 极简 front-matter 解析(只识别 "key: value",零依赖够用)
function parseFrontmatter(text) {
const m = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
const fm = {};
if (!m) return fm;
for (const line of m[1].split(/\r?\n/)) {
const kv = line.match(/^(\w+):\s*(.+)$/);
if (kv) fm[kv[1].toLowerCase()] = kv[2].split('#')[0].trim();
}
return fm;
}
function readManifest(source) {
if (source === 'worktree') {
return fs.existsSync(MANIFEST)
? JSON.parse(fs.readFileSync(MANIFEST, 'utf8')).records
: {};
}
try { // 以 Git HEAD 中的 manifest 为可信基准
const out = execFileSync(
'git', ['show', 'HEAD:.agents/notes/archive-manifest.json'],
{ encoding: 'utf8', stdio: 'pipe' },
);
return JSON.parse(out).records;
} catch {
return undefined; // HEAD 中还没有基准(首次校验 / 首次提交前)
}
}
function verifyFrozenRecords(base, current, readText) {
for (const [file, hash] of Object.entries(base)) {
if (current[file] !== hash)
throw new Error(`封存清单被修改或删除:${file}`);
const text = readText(file);
if (text === undefined || digest(text) !== hash)
throw new Error(`封存正文被修改、移动或删除:${file}`);
}
}
// 1) 活跃目录状态校验:status 必须与目录名一致
function verifyStatusDirs() {
const problems = [];
for (const dir of ['proposed', 'implemented', 'rejected']) {
const dirPath = path.join(NOTES, dir);
if (!fs.existsSync(dirPath)) continue;
for (const f of fs.readdirSync(dirPath).filter(f => f.endsWith('.md'))) {
const fm = parseFrontmatter(fs.readFileSync(path.join(dirPath, f), 'utf8'));
if (!fm.status) problems.push(`${dir}/${f} 缺少 status 字段`);
else if (fm.status !== dir) problems.push(`${dir}/${f} status=${fm.status},应移至对应目录`);
}
}
return problems;
}
// 2) 封存:扫描 archived/,只追加新条目,绝不覆盖旧哈希
function sealArchives() {
const manifest = fs.existsSync(MANIFEST)
? JSON.parse(fs.readFileSync(MANIFEST, 'utf8'))
: { records: {} };
const dirPath = path.join(NOTES, 'archived');
const files = fs.existsSync(dirPath)
? fs.readdirSync(dirPath).filter(f => f.endsWith('.md'))
: [];
for (const f of files) {
const rel = `archived/${f}`;
const hash = digest(fs.readFileSync(path.join(dirPath, f), 'utf8'));
if (manifest.records[rel] !== undefined && manifest.records[rel] !== hash)
throw new Error(`${rel} 已封存但内容变化,须先解冻并重新评审`);
if (manifest.records[rel] === undefined) manifest.records[rel] = hash;
}
fs.writeFileSync(MANIFEST, JSON.stringify(manifest, null, 2) + '\n');
}
// ---- CLI 入口 ----
const readText = rel => {
const p = path.join(NOTES, rel);
return fs.existsSync(p) ? fs.readFileSync(p, 'utf8') : undefined;
};
if (process.argv[2] === '--seal-archives') {
sealArchives();
console.log('✅ 封存完成:archive-manifest.json 已更新');
} else {
try {
const base = readManifest('git');
const current = readManifest('worktree');
if (base) verifyFrozenRecords(base, current, readText);
const problems = verifyStatusDirs();
if (problems.length) {
console.error('🚨 文档门禁未通过:\n' + problems.map(p => ' - ' + p).join('\n'));
process.exit(1);
}
console.log(base
? '✅ 文档门禁通过:状态目录一致,归档哈希完好'
: '✅ 文档门禁通过:状态目录一致(首次校验暂无哈希基准,归档后请运行 --seal-archives)');
} catch (e) {
console.error('🚨 文档门禁未通过:' + e.message);
process.exit(1);
}
}
六、 Agent 协同开发工作流与门禁集成
规则需要落实到日常开发任务的触发节点中。
任务生命周期与执行动作映射表
| 触发节点 | Agent / 开发者执行动作 |
|---|---|
| 开始复杂任务 | 检索 .agents/notes/ 活跃记录(proposed/、implemented/、rejected/),确认当前规则与已被否决的坑点。 |
| 行为/接口变更 | 同次提交更新受影响的当前文档与活跃决策。 |
| 推翻/新增决策 | 同次审查同主题记录,处理部分取代或完全取代关系。 |
| 任务收尾 | 核对提案状态、实际运行证据以及实施边界。 |
| 提交与推送 (CI) | 执行代码与文档脚本门禁,校验状态匹配与防篡改哈希。 |
Git Hook 与 CI 集成
两条命令覆盖全部日常场景:
bash
# 日常校验:状态目录一致性 + 归档哈希完整性
node scripts/verify-docs.mjs
# 新归档后封存:扫描 archived/ 并追加哈希清单
node scripts/verify-docs.mjs --seal-archives
接入 pre-commit(提交前自动拦截):
bash
# 1. 在仓库根目录创建 .githooks/pre-commit,内容一行:
# node scripts/verify-docs.mjs
# 2. 启用自定义 hooks 目录(每台机器执行一次)
git config core.hooksPath .githooks
接入 GitHub Actions(推送与 PR 时兜底校验):
yaml
# .github/workflows/docs-gate.yml
name: docs-gate
on: [push, pull_request]
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: node scripts/verify-docs.mjs
七、 存量项目治理落地方案(6 步迁移指南)
针对已经积累了大量无序文档的老旧项目,按领域逐步推进,一次只治一个领域:
- 领域划定:圈定一个业务领域,搜索该领域的代码、配置、现有文档与决策记录,找出当前所有者。
- 事实核对:逐份检查提案状态与实施范围,区分"当前事实"与"历史证据"。
- 提取迁移:区分部分取代与完全取代,先把仍有效的独有理由迁移到新记录。
- 引用修复:全仓库搜索入站引用并修复,然后合并、删除或移入归档区。
- 签名封存 :执行
node scripts/verify-docs.mjs --seal-archives生成哈希清单,锁死归档。 - 门禁护航 :接入 pre-commit 与 CI(见第六节),并在
AGENTS.md固化检索规则。
八、 借鉴 DeepSeek Harness (dsh) 的治理启示
在 DeepSeek Harness (dsh) 等治理实践中,有三点被反复验证:
- 废弃集中式 INDEX.md:拒绝手动维护集中状态列表文件,避免产生新的维护负担和同步偏差;通过生命周期目录树直接定位。
- 语义判断高于物理指标:文档的保留、删除或归档,应完全基于"未来决策价值"进行语义判断,不能简单用创建年龄或字数来决定。
- 变更原子性:根规范要求"代码变更必须伴随相关文档更新",并由 CI 静态门禁进行审核校验。
为什么不直接用 MADR / adr-tools
现有 ADR 工具链(MADR 模板、adr-tools)面向人类团队的评审流程 :状态写在正文里,靠人阅读维护。本方案的服务对象是 Agent,差异在三点------状态即目录 (检索时按目录天然过滤终态记录)、取代关系进 front-matter (可被 CI 机器校验,而不是靠人读)、归档区哈希冻结(防 Agent 顺手改写历史)。两者并不互斥:单条记录的正文结构完全可以沿用 MADR 模板。
总结
一个长期 AI Agent 项目的健壮性,不仅取决于代码编写的速度,更取决于其决策记忆系统的清洁度。
通过建立 事实-规则-依据 三层解耦架构,配合 目录即状态的 ADR 状态机 与 SHA-256 哈希门禁,废弃文档对 Agent 上下文的干扰可以被系统性消除------从"祈祷 Agent 别翻到旧文档",变成"旧文档物理上翻不到、翻到了也过不了门禁"。
从今天的下一次提交开始:先建四个目录,再抢救三条最危险的旧决策。