【Agent 架构实战】大模型长期项目开发:决策文档生命周期管理与 CI 门禁治理

【Agent 架构实战】大模型长期项目开发:决策文档生命周期管理与 CI 门禁治理

如果你也在用 Agent 维护长期项目,大概率撞过这样的墙:上个季度就废弃的刷新策略,Agent 偏偏检索到旧文档,理直气壮地写回代码;你问它为什么,它还贴出"出处"证明自己有据可查。问题不在 Agent 记性差,而在仓库从未用机器可读的方式表达过"哪些文档还算数"。本文给出一套零依赖、可直接抄走的方案:三层知识解耦、四状态决策目录、一条 SHA-256 哈希门禁。

适合谁 :用 Claude Code / Codex / Cursor 等 Agent 参与数月以上周期项目开发的工程师

解决什么 :废弃文档被 Agent 误检索导致的逻辑冲突、状态漂移与历史改写

读完得到:一套"目录即状态机"的 notes 结构 + 可直接运行的门禁脚本 + 存量项目 6 步迁移路径

文章目录

  • [【Agent 架构实战】大模型长期项目开发:决策文档生命周期管理与 CI 门禁治理](#【Agent 架构实战】大模型长期项目开发:决策文档生命周期管理与 CI 门禁治理)
    • 太长不看:五分钟最小方案
    • [一、 核心痛点:为什么长期 Agent 项目的文档容易"腐烂"?](#一、 核心痛点:为什么长期 Agent 项目的文档容易"腐烂"?)
    • [二、 决策记录判定标准:什么内容值得写?](#二、 决策记录判定标准:什么内容值得写?)
      • [【图解 2:价值三问判定流程】](#【图解 2:价值三问判定流程】)
      • [值得记录 vs 避免记录的范围对比](#值得记录 vs 避免记录的范围对比)
    • [三、 ADR 状态机模型与生命周期流转](#三、 ADR 状态机模型与生命周期流转)
      • [记录格式:Front-matter 最小规范](#记录格式:Front-matter 最小规范)
      • [【图解 3:决策文档状态转换流转】](#【图解 3:决策文档状态转换流转】)
      • 状态转换三大核心避坑守则
    • [四、 部分取代与完全取代的精准处理机制](#四、 部分取代与完全取代的精准处理机制)
      • [【图解 4:文档更新处理分支】](#【图解 4:文档更新处理分支】)
    • [五、 防篡改机制:校验归档原文与 SHA-256 摘要](#五、 防篡改机制:校验归档原文与 SHA-256 摘要)
    • [六、 Agent 协同开发工作流与门禁集成](#六、 Agent 协同开发工作流与门禁集成)
    • [七、 存量项目治理落地方案(6 步迁移指南)](#七、 存量项目治理落地方案(6 步迁移指南))
    • [八、 借鉴 DeepSeek Harness (dsh) 的治理启示](#八、 借鉴 DeepSeek Harness (dsh) 的治理启示)
      • [为什么不直接用 MADR / adr-tools](#为什么不直接用 MADR / adr-tools)
    • 总结

太长不看:五分钟最小方案

不想一次吃下整套体系?先做这三件事,就能挡住大部分 Agent 误读:

  1. 建目录 :.agents/notes/ 下建 proposed/、implemented/、rejected/、archived/ 四个空目录;
  2. 抢救高危记录 :把手里最容易被 Agent 误读的 3 个旧决策 写成 implemented/ 记录(front-matter 只要 id / status / date 三个字段,格式见第三节);
  3. 改一行规则 :在 AGENTS.md 里加一句硬规则------"中等以上任务开工前,先按领域关键词检索 .agents/notes/ 下的 proposed/、implemented/、rejected/ 三个目录"。

到这一步没有任何脚本和 CI,纯约定就能解决八成误读。后文的 SHA-256 哈希门禁是给多人协作、高频提交团队的加固项,可以等痛点真的出现再上。

一、 核心痛点:为什么长期 Agent 项目的文档容易"腐烂"?

当一个项目持续由 AI Agent 参与开发数月后,代码库内通常会积累大量的设计说明、实施计划与决策记录。最初这些文档提供了宝贵的上下文;但随着业务迭代,它们往往会变成新的问题源头:

  1. 事实与历史混淆:代码已采用新方案,旧文档仍写着原方案有效。
  2. 状态漂移:提案早已落地,文件却停留在待评审(Draft/Proposed)目录。
  3. 碎片化与覆盖冲突:同一个问题先后写了三份记录,各自保留不同的细节,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/ 只进不改------封存后的正文与路径被哈希锁死(第五节),历史原文不随现状变化,新事实永远写在活跃目录并单向引用历史。

状态转换三大核心避坑守则

  1. 已完成 ≠ 应当归档 :一个安全边界即便已实施多年,只要它依然指导每次相关修改,就必须留在 implemented/ 活跃目录。
  2. 没实施过的提案,归宿是 rejected/ 而不是 archived/ :archived/ 只收"曾经真实实施过、后被取代"的历史;从未落地的想法不产生历史价值。
  3. 曾经实施 ≠ 标为 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 步迁移指南)

针对已经积累了大量无序文档的老旧项目,按领域逐步推进,一次只治一个领域:

  1. 领域划定:圈定一个业务领域,搜索该领域的代码、配置、现有文档与决策记录,找出当前所有者。
  2. 事实核对:逐份检查提案状态与实施范围,区分"当前事实"与"历史证据"。
  3. 提取迁移:区分部分取代与完全取代,先把仍有效的独有理由迁移到新记录。
  4. 引用修复:全仓库搜索入站引用并修复,然后合并、删除或移入归档区。
  5. 签名封存 :执行 node scripts/verify-docs.mjs --seal-archives 生成哈希清单,锁死归档。
  6. 门禁护航 :接入 pre-commit 与 CI(见第六节),并在 AGENTS.md 固化检索规则。

八、 借鉴 DeepSeek Harness (dsh) 的治理启示

在 DeepSeek Harness (dsh) 等治理实践中,有三点被反复验证:

  1. 废弃集中式 INDEX.md:拒绝手动维护集中状态列表文件,避免产生新的维护负担和同步偏差;通过生命周期目录树直接定位。
  2. 语义判断高于物理指标:文档的保留、删除或归档,应完全基于"未来决策价值"进行语义判断,不能简单用创建年龄或字数来决定。
  3. 变更原子性:根规范要求"代码变更必须伴随相关文档更新",并由 CI 静态门禁进行审核校验。

为什么不直接用 MADR / adr-tools

现有 ADR 工具链(MADR 模板、adr-tools)面向人类团队的评审流程 :状态写在正文里,靠人阅读维护。本方案的服务对象是 Agent,差异在三点------状态即目录 (检索时按目录天然过滤终态记录)、取代关系进 front-matter (可被 CI 机器校验,而不是靠人读)、归档区哈希冻结(防 Agent 顺手改写历史)。两者并不互斥:单条记录的正文结构完全可以沿用 MADR 模板。

总结

一个长期 AI Agent 项目的健壮性,不仅取决于代码编写的速度,更取决于其决策记忆系统的清洁度。

通过建立 事实-规则-依据 三层解耦架构,配合 目录即状态的 ADR 状态机 与 SHA-256 哈希门禁,废弃文档对 Agent 上下文的干扰可以被系统性消除------从"祈祷 Agent 别翻到旧文档",变成"旧文档物理上翻不到、翻到了也过不了门禁"。

从今天的下一次提交开始:先建四个目录,再抢救三条最危险的旧决策。

相关推荐
rolt3 小时前
智能机床-06 机械 ISO 23704-4-2026-用UML表示的行业标准
软件工程·产品经理·架构师·uml
制造数据与AI践行者老蒋4 小时前
智联工坊实战:工业数据质量自动检测方案 3σ 原则 + Agent 编排 + 分层容错完整实践
数据治理·ai agent·智能工厂·工业大数据·python实战·制造业数据·数据质量巡检
️公子5 小时前
Claude Code 2.1.289 补上四个 deny 缺口:Agent 权限匹配器的规范化、分层裁决与回归语料
软件工程·ai agent·权限控制·claude code·安全工程
EatFan19 小时前
从“框架混战“到“运行时收敛“:2026 年 AI Agent 开发框架的三条路线之争
java·数据库·人工智能·多智能体·ai agent·mcp·agent 框架
Zelman21 小时前
测试过程模型与左移右移
测试·自动化运维·devops
诺伦1 天前
AI Agent编排实战:用四层架构搭建增长运营垂类Agent系统 | RiseClaw玄策
人工智能·ai agent·mcp·agent编排·增长运营
EatFan1 天前
从云原生到AI原生:2026后端架构“三驾马车”(事件驱动、虚拟线程、AI Agent内嵌)演进解析
spring boot·云原生·架构·虚拟线程·ai-native·ai agent·spring ai
EatFan2 天前
AI Agent 进入工程化下半场:从多智能体编排走向治理、标准化与运行沙箱
人工智能·多智能体·ai agent·开源框架·mcp·agents.md
code2cat2 天前
【随笔】MCP缓存期限与共享范围:让Agent复用资料时记住边界
java·后端·缓存·ai agent·mcp