---
name: contract-pass-workflow
description: 合同审查pass后的标准操作——更新tracker、更新xlsx清单、上传Nextcloud、清缓存。Doro说pass后照此执行。
version: 1.0.0
tags: [合同审查, pass, tracker, xlsx]
---

# 合同审查 Pass 后标准操作

## 交付文件位置（铁律，2026-06-29 Doro纠正）

所有交付文件统一放在 **`Doro合同审查任务/任务交付/`（根目录）**，不放顾问单位子文件夹。

- ✅ `Doro合同审查任务/任务交付/【修】合同_朱家角.docx`
- ❌ `Doro合同审查任务/朱家角镇社区卫生服务中心/任务交付/【修】合同_朱家角.docx`

workflow YAML deliverer 步骤（第210行）明确写的上传路径是根目录 `任务交付/`。如果 deliverer 或 final_review 把文件放到了子文件夹，pass 步骤0核对时必须发现并移到根目录。

## 步骤0核对要点

pass 步骤0的核对流程：
1. 取原始文件名（去掉【修】/【审】前缀）
2. 去 `Doro合同审查任务/待审查/` 确认源文件存在
3. **确认交付文件在根目录 `任务交付/`**（不是子文件夹）
4. 顾问单位核对（从合同正文读甲方名称）
5. tracker 查重

## 红线：验证指令 ≠ 回忆指令（2026-07-02 信任危机后确立）

### 根因排查铁律：抛弃旧判断，先查“发生了什么变化”，再谈原因（2026-07-14 Doro纠正）

当 Doro 明确说“去查原因”“不是让你猜测、推断”“完全抛弃此前的判断，重新核查情况”时，后续动作必须切换为**变化核查模式**，而不是继续打磨措辞、修补上一版归因。

**强制步骤：**
1. **旧判断全部作废**：停止沿用“状态漂移”“异常”“习惯变差”“元控制失效”等任何解释性语言，除非已经有直接证据支持。
2. **先限定时间窗**：如果用户已给出时间边界（如“发生在 7 月 10 日前”），所有核查必须围绕该边界展开；边界外的改动先排除，不得混入结论。
3. **优先查“可观察变化”而不是“解释”**：
   - 配置文件修改时间与具体改动；
   - workflow/queue/watchdog/notify 脚本的修改时间与新增逻辑；
   - skill 规则文本的修改时间；
   - gateway / auto-notify / watchdog 的退出、重启、恢复日志；
   - tracker / queue / done / manifest 等状态文件是否出现结构变化。
4. **汇报格式必须是“已查到的变化 / 没查到的变化”**，不能把“更可能”“像是”“说明了”写成原因结论。
5. **只有在“变化事实”查清后，才允许进入第二步根因分析**；若变化事实尚未闭环，明确说“目前只查到这些变化，原因尚未下结论”。

**禁止事项：**
- 不断修改上一版判断的措辞，假装自己在继续调查；
- 用“异常”“偶发”“状态不好”“坏习惯”去解释持续两天、批量失效的问题；
- 把证据和推断混写成同一层结论；
- 用户要求查原因时，实际只做语言收缩而不做新核查。

**本会话教训：** Doro连续纠正“不是让你猜测、推断”“不是让你不断修改措辞”“要你完全抛弃此前判断，重新核查 7 月 10 日前发生了什么变化”。以后凡是原因排查任务，第一步不是解释，而是建立时间窗并枚举已发生变化。

### 先查清时间窗，再查数据（2026-07-13 本会话再犯后补丁）

当 Doro/Maggie 说“上周五”“今天”“昨天”“本周一”这类**相对日期**时，**第一步不是直接查数据，而是先把自然语言时间词锚定成明确的北京时间起止窗口**。没先锚时间窗，就会出现：
- 把“上周五”理解成错误日期；
- UTC/BJT 边界算错；
- 用错窗口后得出“0 份”之类错误结论；
- 之后再补查 gateway.log 才发现用户是对的，严重伤信任。

**强制步骤：**
1. 先用北京时间确认当前日期和星期；
2. 把“上周五/今天”等词转换成**北京时间明确起止时间**；
3. 再换算成 UTC 时间戳/窗口；
4. 把这个时间窗写出来后，才开始查 `.meta` / gateway.log / tracker / xlsx。

**执行纪律：**
- 如果 `meta` 查不到，但 `gateway.log` 里已经出现该日文件消息证据，**不得继续说“0 份”或“没发”**；必须当场降级结论为“已证明确实发了，但文件清单仍在继续反查”。
- 对周五这类历史批次，**至少要交叉两源**：`gateway.log` 文件消息 + tracker / xlsx / queue 痕迹，不能只信一侧。
- 没闭环前，结论只能说“已查实部分”“尚未查实部分”，不能提前给“全部无遗漏”的总判断。

**本会话教训**：周五邱律师明明发了文件，但因先把时间窗算错、又只依赖 `.meta`，错误说成“0 份”；之后从 `gateway.log` 查到 `msg=''` 文件消息才纠正。以后凡是相对日期统计任务，**先定北京时间窗口，再查数据**。

## 红线：验证指令 ≠ 回忆指令（2026-07-02 信任危机后确立）

### 当前交付目录全量 pass 指令（2026-07-13 Doro明确授权）

当 Doro 明确下达类似指令：
- "任务交付文件夹里所有的合同及companion，都做pass"
- "现在这一刻，Nextcloud-任务交付里，所有的合同和companion，都做pass流程"

这属于**对当前任务交付目录的全量授权**，不再局限于单份合同或单个 companion。此时必须：

1. **先列出任务交付目录当前全部文件**，不要凭上一条对话里提到的那一份合同推断。
2. **合同与 companion 都要纳入核查范围**——先查清目录里到底有哪些主合同、有哪些 companion，不能只扫主合同关键词就下结论。
3. **主合同与 companion 的 pass 处理规则不同**：
   - **主合同**：做完整 pass——tracker 标记 `completed`，并登记到 excel。
   - **companion**：属于主合同的附属交付物，**不单独登记到 excel，不单独占 seq**。只在核查结果中确认其存在与关联关系，必要时体现在主合同的 pass 备注/关联记录中。
4. **先查 tracker / xlsx 再写入**，但用户已经授权时，不要卡在"没有 tracker 记录所以我先不做"的保守口径上；对主合同应直接补建记录并完成 completed + excel 登记。
5. **汇报时先给出全量清单，再区分主合同与 companion 的处理结果**，不能把“全部做pass”误写成“所有文件都单独登记 excel”。

### 本次会话教训
- 错误做法：只围绕白鹤这一份合同回答"已pass"，没有先把任务交付目录全量列出，遗漏了香花桥和朱家角积分项目的一整组合同及 companion。
- 第二层错误：把 companion 当成主合同一样单独写入 tracker/xlsx，导致 seq 冲突和错误登记。
- 正确做法：当 Doro 说"任务交付文件夹里所有的合同及companion，都做pass"时，必须把**当前任务交付目录的全部文件**作为审计范围，但**excel 只登记主合同，companion 不单独登记**。
**凡Doro说"查""核实""核对""是不是都""有没有遗漏"→ 回复中必须先有工具调用再有结论。context记忆/session summary ≠ 查证，不可直接输出。**

违反后果：Doro已明确说"到了无法信任你的程度"。这不是规则问题，是行为问题——规则早就写了（见已知坑），照样违反。

四个具体失败模式（2026-07-02 同一轮对话全犯）：
1. **拿记忆当查证**：session context有信息→直接组织成答案→包装成"查实结果"→其实没跑工具
2. **渠道遗漏**：只查了QiuTing私信，漏了Doro私信、Doro直接Nextcloud上传、飞书等渠道
3. **不认识自己的输出**：之前汇报过的数据（seq 216-222含重固），被追问时说"找不到"——数据一直在，没看自己历史输出
4. **推责给用户**：找不到时直接问Doro"你记得叫什么名字吗"——把验证责任转嫁给用户。正确做法：穷尽搜索策略（换关键词、按时间批次找、按相邻seq推断、直接读xlsx逐行扫、session_search换多个query）。数据就在tracker里，是自己没认真遍历。

**第4条补充（2026-07-02追加）**：被追问"你真的查了吗"→ 如果上一条回复里没有tool call，直接承认"没查，现在查"。不辩解、不包装。

**唯一可接受的行为**：
- 跑工具 → 看输出 → 写结论
- 不确定的说不确定
- 被追问时不防御不绕，直接承认没查到
- 对比自己之前输出过的表格/数据，确认是否已经有答案

## 恢复到workflow交付状态（2026-07-13 教训）

Doro可能要求"恢复到workflow完成的状态"——意思是撤销你的手动修改，让NC上的交付文件回到workflow原始交付版本。

**恢复方法（按优先级）**：
1. **NC版本历史**：`docker exec nextcloud-nextcloud-1 ls /var/www/html/data/doro/files_versions/Doro合同审查任务/任务交付/` 找 `.v<timestamp>` 文件，最早的版本 = workflow原始交付版
2. **`/tmp/pass_check_*` 副本**：pass流程核对时从NC拉取的副本，如果时间早于你的手动修改，就是workflow原版
3. **练塘镇子目录副本**：部分合同在 `练塘镇社区卫生服务中心/任务交付/` 也有一份

**操作**：docker cp 覆盖 → chown www-data → occ files:scan。

**场景**：你手动修复了workflow交付的合同（补编号、修字体等），但Doro认为应该重新走workflow而非手动修补。恢复后等Doro进一步指示。

## 前置审查（Doro要求"审查workflow修改"时触发）

Doro可能在pass之前要求逐份审查workflow交付物的质量。这不是pass流程，而是**前置质量审计**，只有审计通过+手动修复后Doro才会说pass。

### 审计方法论

1. **区分原文修订 vs workflow修订**：用zipfile读XML，按`w:ins/@author`和`w:del/@author`区分。`author=WB`是workflow的，其余（WB-1/86187/杨丽等）是原文自带的→保持不动。
2. **对照原文确认**：必须docker cp原文件（从NC待审查目录），转换后逐段对比，确认哪些修订是原文自带。
3. **逐项检查清单**：
   - INS rFonts：WB INS的rFonts属性必须与同段原文run一致（不多不少）。常见问题：多了hAnsi/cs/hint
   - pStyle：新增条款标题的段落样式必须与原文条款标题一致（如Heading4），不能用Style15等其他样式
   - 标题空格：原文"第六条违约责任"无空格→新增也不能有空格"第七条转包与分包"
   - 子编号顺延：章节编号改了（七→八），内部子编号（7.1→8.1）也必须改
   - 赔偿上限：双向条款的赔偿上限（限制甲方获赔）按规则"能删就删"
   - 内容去重：新增保密存续等条款前，检查原文是否已有同义表述
   - 脚注"法律顾问修订版"：**必须用修订格式**（w:ins, author=WB），不能是普通文本
   - 文件命名：【修】+原文件名一字不动（含扩展名变化.doc→.docx）
4. **通读全文**：接受修订后的全文必须通读，检查WB插入的内容是否与上下文语句通顺、有无重复编号（原文问题不改，但要识别）
5. **Doro的期望**：
   - "打开文件查清楚再回答我"→ 必须用工具完整检查后才能下结论，不能凭印象
   - "看清楚前后文再回答我"→ 报告问题前必须理解修改点的上下文语境
   - "有没有该加粗没加粗的"→ 格式检查要全面（bold/style/indent/spacing都要对比）
   - "按照workflow的规则，手动修改"→ 发现问题后直接修复，不只是报告

### 修复操作要点

- 用zipfile+lxml直接操作XML（不用python-docx修改tracked changes）
- 修复后的INS rFonts只保留原文有的属性（通常eastAsia+ascii）
- 子编号顺延：原文数字可能分散在多个run中（如"7"+".1  "两个run），只需DEL+INS第一个数字run
- P49类大段文本需精确split run（保留前后文，只DEL中间要删的部分）
- 修复后必须python-docx打开验证

## 触发条件
Doro对已交付的合同**明确说"pass"**（可以是单份或批量）。

⚠️ **绝对前提：Doro必须亲口说"pass"才能启动此流程。** 合同交付后、workflow完成后、端午节合同end后——这些都**不是**pass。不要因为合同已交付就主动查xlsx/tracker或准备pass操作。Doro没说pass之前，对交付物的一切后续操作（更新tracker、更新xlsx、查重等）都不做。

⚠️ **"我满意了"≠Doro说pass（2026-07-13教训）**：即使你作为审查负责人完成了质检、修复了所有问题、对文件满意，也**绝不能自行执行pass流程**。必须等Doro明确说"pass"。2026-07-13实证：白鹤劳务派遣协议修复完毕后自行做了pass，被Doro纠正"我没说pass你做什么pass"，紧急撤销（tracker回退delivered+xlsx删行）。自主质检和pass是两个完全独立的步骤——前者是你的职责，后者是Doro的权力。

2026-06-15教训：两次被Doro纠正（"我都还没说pass呢"、"今天的合同我都没说pass呢"）——agent在合同交付后主动查xlsx是否已更新、准备补写tracker，被视为越权操作。
2026-07-13教训：白鹤劳务派遣协议自行质检完后直接执行pass，被纠正"我没说pass你做什么pass"，紧急撤销。

**手动审查交付物的完整检查清单**见 `references/manual-review-checklist-0713.md`——当Doro要求"审查workflow修改的情况"时按此执行。核心：自主质检→发现问题直接修→报告→等Doro说pass。不问"需要修复吗"，不自行pass。

## 特殊判定："终身不通过"

Doro可能对某些合同说"终身不通过"——这意味着合同审查质量太差，**永远不会pass**。
- **不是返工**：不是让你修了再交，而是直接否决
- **处理方式**：在tracker中标记`status: "permanently_rejected"`，不进入pass流程
- **反思**：必须分析为什么质量差到这个程度，是workflow哪个角色出了问题，把教训记入historical-failures
- **不要追问Doro**：已经定性了就不要再烦，自己复盘

## 操作步骤（严格按顺序）

### 0. 交付物核对（写入前必做）

⚠️ **PDF 批注交付件的独立核验** → 用 `scripts/verify_pdf_annotation_deliverable.py <原件> <NC交付件> [本地核验件]`：一键查 SHA256 字节一致、页数、原文未改动、批注数/author=WB、批注格式（"建议"开头无【】）、高亮几何锚定。扫描件/PDF 合同走批注模式（非 docx 修订），docx 专项检查不适用，用此脚本替代。

Doro说pass后，在写tracker/xlsx之前，**必须先核对交付物与源文件的匹配关系**：

1. **取原始文件名**：从交付文件名去掉`【修】`或`【无修改意见】`前缀 → 得到原始文件名
2. **待审查目录核实**：去Nextcloud `Doro合同审查任务/待审查/` 确认该原始文件名存在。不存在则停下来排查，不继续写入
3. **顾问单位核对**：确认要写入xlsx的顾问单位名称正确。⚠️ 不能盲信classifier的`our_party_name`——已有多次classifier误判案例（如智慧医院云项目合同classifier设为"朱家角"实际甲方是"卫健事业发展中心"）。**必须用python-docx打开合同原文，直接读取甲方名称**
   - **⚠️ python-docx 的 `paragraph.text` 会吞掉 `w:ins`（修订插入）内容 → 读出的甲方可能是"补全前"的残缺名（2026-06-25 实证）**：当本次审查的改动之一就是"给甲方补全行政区前缀"（如 WB 修订插入「上海市青浦区」），交付件里甲方全称是「原稿可见文字 + w:ins 插入文字」拼起来的。`python-docx` 遍历 `doc.paragraphs[i].text` 时**未必包含 ins 的文字**，会让你误以为甲方还是缺前缀的简称。**核甲方全称必须把 `w:ins` 算进去**：用 zipfile 读 `word/document.xml`、对甲方那一段 `''.join(t.text for t in p.iter('{...}t'))`（`w:t` 不分 ins/非 ins，全收），或干脆遍历所有 `w:ins` 看 author=WB 插了什么。2026-06-25 项目终止协议书：`paragraph.text` 显示甲方「华新镇社区卫生服务中心」，实际交付件 w:ins 补了「上海市青浦区」，全称应是「上海市青浦区华新镇社区卫生服务中心」——只看 `.text` 就会把残缺简称写进 tracker/xlsx。**写 party 前先确认你读的是"接受修订后"的完整甲方名。**
   - **顾问单位名称空白的特殊情况（按合同来源分两路，2026-06-25 补全）**：合同正文里顾问单位名称是空白下划线（待签时填）时，无法从正文确定。**先分清这份合同是谁的任务线**，再决定问谁——别默认是邱律师：
     - **邱律师批量线**（health-centers）：私信邱律师询问归属。私信用 `python3 ~/.hermes/scripts/wecom_dm.py --to qiuting --text "…"`（或 `_send_wecom(extra,'QiuTing',msg)`），**不用** `send_message(target='wecom:X')`（静默回退 home channel）。暂停 pass（不写 tracker/xlsx），邱律师回复后从步骤1继续；Doro 说过"邱律师回复了就直接补上 我不管了"——回复后自主补登记，不再找 Doro。
     - **Doro 直接指派 / 非批量线**（如幼儿园保密协议、劳动合同等非卫生中心合同）：**问发起 pass 的人本人**（通常就是正在对话的 Doro），不要问邱律师，也不要自己瞎填占位符。
     - ⚠️ **绝不用泛指占位符当 party 写进 tracker/xlsx**（如"幼儿园（园方，名称空白待填）"）——2026-06-25 教训：园名空白我写了这种占位符，Doro 直接纠正"顾问单位是平和学校"。空白就停下来问准确**法律主体全称**，确认后再写。
   - **同名/近名主体消歧（2026-06-25 教训，写 party 前必做）**：拿到顾问单位名后，先 `openpyxl` 扫 xlsx 第 C 列看清单里**是否已有多个相似名**的主体——它们往往是**不同法律实体**，不能混用。本会话清单里同时有「上海青浦平和**幼儿园有限公司**」(seq13/15) 和「上海青浦平和**双语学校**」(seq44/210/211)，一个园、一个校，是两个主体。判别靠**合同内容性质**：这份通篇"幼儿园/幼儿就读/保教费"→幼儿园主体；但既然清单里并存多个近名实体，**最终仍用 `clarify` 让发起人拍板一个准确全称**，并复用清单里既有的写法（保持前后一致，别造新写法）。
   - **更正已写错的 party（两处同步）**：若 tracker/xlsx 已写入后才被纠正，两处都要改：tracker 用原子写回（tempfile+rename）改对应 seq 的 `party`；xlsx 改第 C 列后重新 `docker cp` 上传 + `occ files:scan` + 清 OnlyOffice 缓存重启；最后从线上重新拉 xlsx + 读 tracker **核对两处一致**才算完成。
4. **查重**：在tracker中检查是否已有相同 `original_filename` + `party` 的completed记录。有则为重复交付，不再写入

以上4步全部通过，才进入步骤1写tracker。任何一步不通过，停下来排查原因。

**注意**：Doro批量pass时可能列出审查意见文件（如"朱家角审查意见-xxx"、"采购协议-审查意见"）。这些是主合同的附属交付物，**默认不单独写tracker/xlsx条目**。只需为主合同文件（【修】前缀的）写tracker和xlsx。审查意见文件如果不在交付目录中（可能已被清理或因classifier误判未生成），不影响主合同的pass流程——跳过即可，不需要报错或追问Doro。

### 例外：用户明确说“任务交付文件夹里所有的合同及companion，都做pass流程”时（2026-07-13 Doro明确）
当 Doro 用**当前任务交付目录全量授权**的口径下指示：
- “任务交付文件夹里所有的合同及companion，都做pass”
- “现在这一刻，Nextcloud-任务交付里，所有的合同和companion，都做pass流程”

此时必须把 companion **纳入 pass 核查范围**，但仍要遵守：
- **companion 不是独立合同，不单独登记 excel，不单独占 seq**；
- tracker/xlsx 的登记主体仍然是**主合同**；
- companion 的处理应当体现在“该主合同已连同 companion 一并核查/补做pass”的结果里，而不是把每个 companion 当主合同单独建台账。

**执行顺序**：
1. 先把当前 `任务交付/` 目录全部文件列出来，不能只围绕当前对话那一份合同；
2. 再识别哪些是主合同、哪些是 companion；
3. 主合同逐份做 pass / tracker / xlsx；
4. companion 只做挂靠核查，不单独占 excel 行；
5. 回复时明确区分“主合同已登记几份、companion 已核查几份”，不要混成一类。

**2026-06-11教训**：安全测试合同因跳过核对，第一次把原始文件名写进xlsx文件名列（没有【修】前缀），发现后补写但未清理错误记录，导致tracker和xlsx各多一条重复数据。
**2026-06-12教训**：手动写xlsx时列顺序写反（日期和序号对调、文件名列写成原始文件名而非delivered_filename、顾问单位列写成"已完成"状态文字）。**写xlsx前必须先读上一行确认列顺序**，不要凭记忆。正确列顺序：A=序号, B=日期, C=顾问单位, D=合同名称, E=文件名(delivered_filename)。

### 1. 更新 Tracker JSON

路径：`~/.hermes/data/contract-tracker.json`

#### 状态流转（新增 `delivered` 中间态，2026-07-02 确立）

```
文件到达 → workflow审查 → deliverer交付成功 → queue-runner写入 status=delivered
                                                       ↓
                                          Doro说pass → status=completed
                                                       ↓
                                              24h后 → cleanup清理
```

**三道防线防重复：**
| 检查点 | 逻辑 |
|--------|------|
| queue-runner 启动前 | 查 tracker，`delivered` 或 `completed` → 直接 SKIP 并移入 done/ |
| watchdog resume 前 | 查 tracker，已交付的 thread 不恢复 |
| deliverer 写入时 | exists 检查，同文件不重复写入 |

#### Pass 时的写入逻辑

**如果 tracker 中已有该 `original_filename` 且 `status=delivered`** → 更新该记录为 `completed`，补全 `seq`/`party`/`contract_name`/`delivered_filename`/`converted_filename`/`xlsx_updated_at`/`cleaned=false`。

**如果 tracker 中没有记录**（旧合同、手动审查等情况）→ 新建完整记录，直接 `status=completed`。

完整记录示例：
```json
{
  "original_filename": "消防设施检测服务合同（练塘卫生院）.doc",
  "delivered_filename": "【修】消防设施检测服务合同（练塘卫生院）.docx",
  "converted_filename": "消防设施检测服务合同（练塘卫生院）.docx",
  "party": "上海市青浦区练塘镇社区卫生服务中心",
  "contract_name": "消防设施2026年度检测服务合同",
  "seq": 175,
  "status": "completed",
  "delivered_at": "2026-06-09T18:30:00+08:00",
  "xlsx_updated_at": "2026-06-09T20:09:00+08:00",
  "cleaned": false
}
```

字段说明：
- `original_filename`：邱律师发来的原始文件名（可能是.doc）
- `delivered_filename`：交付到任务交付目录的文件名（【修】前缀）
- `converted_filename`：workflow转换后的.docx文件名（原始是.doc时有值，否则空字符串）。**来源**：workflow的`converted_filename`字段会从classifier一路传递到deliverer/final_review输出。pass流程必须从workflow输出中提取并写入tracker。如果workflow输出中没有此字段（旧workflow跑的合同），则根据original_filename判断：以`.doc`结尾的，converted_filename = 同名`.docx`；以`.docx`结尾的，converted_filename = 空字符串
- `seq`：合同审查清单中的序号
- `delivered_at`：workflow 交付完成时间（queue-runner 写入）
- `xlsx_updated_at`：pass 时写入，北京时间ISO格式，清理脚本据此计算24h
- `cleaned`：清理脚本执行后改为true

**写入方式**：先写临时文件再rename（原子操作），防止进程中断导致JSON损坏。

```python
import json, os, tempfile
from datetime import datetime, timezone, timedelta

BJT = timezone(timedelta(hours=8))
tracker_path = os.path.expanduser('~/.hermes/data/contract-tracker.json')

# 读取现有tracker
if os.path.exists(tracker_path):
    with open(tracker_path, 'r') as f:
        tracker = json.load(f)
else:
    tracker = {"contracts": []}

now = datetime.now(BJT).isoformat()

# 查找是否已有 delivered 记录
existing = None
for c in tracker["contracts"]:
    if c.get("original_filename") == original_filename:
        existing = c
        break

if existing and existing.get("status") == "delivered":
    # 已有 delivered → 更新为 completed（补全字段）
    existing["status"] = "completed"
    existing["delivered_filename"] = delivered_filename
    existing["converted_filename"] = converted_filename
    existing["party"] = party
    existing["contract_name"] = contract_name
    existing["seq"] = seq
    existing["xlsx_updated_at"] = now
    existing["cleaned"] = False
elif existing and existing.get("status") == "completed":
    # 已经 completed → 查重命中，不重复写入
    pass
else:
    # 无记录 → 新建（旧合同/手动审查走这条路）
    tracker["contracts"].append({
        "original_filename": original_filename,
        "delivered_filename": delivered_filename,
        "converted_filename": converted_filename,
        "party": party,
        "contract_name": contract_name,
        "seq": seq,
        "status": "completed",
        "xlsx_updated_at": now,
        "cleaned": False
    })

# 原子写入
fd, tmp = tempfile.mkstemp(dir=os.path.dirname(tracker_path), suffix='.json')
with os.fdopen(fd, 'w') as f:
    json.dump(tracker, f, ensure_ascii=False, indent=2)
os.replace(tmp, tracker_path)
```

### 2. 更新合同审查清单 xlsx

路径（Nextcloud）：`Doro合同审查任务/合同审查清单.xlsx`

⚠️ **铁律（2026-07-03覆盖事故后确立）：写xlsx前必须从Nextcloud实时拉取最新版本，禁止使用/tmp或本地任何已有的xlsx文件。** 违反此规则会用旧版覆盖新版，导致其他session已登记的记录丢失（2026-07-03实证：用240行旧文件覆盖了252行最新版，丢失12条记录）。

**强制检查流程**：
1. `docker cp` 从NC拉取 → 保存到 `/tmp/合同审查清单_LIVE.xlsx`（带LIVE后缀避免与残留文件混淆）
2. 打开后先读 `ws.max_row` 和最后一行的seq → 打印确认
3. 如果发现本地已有同名文件，**必须删除后重新拉取**，不得复用
4. 追加新行后上传

步骤：
1. **从Nextcloud下载最新xlsx（必须实时拉取，禁止复用本地文件）**
2. 用openpyxl追加行（序号、日期、顾问单位全称、合同名称、文件名）
3. 从上一行复制字体和对齐样式（用copy()）
4. border用Side对象重建（不用ref_cell.border直接赋值，会报unhashable错误）
5. 保存后上传回Nextcloud
6. 执行files:scan + 清OnlyOffice缓存

```bash
# 下载（铁律：必须每次实时拉取，rm掉旧文件防止复用）
rm -f /tmp/合同审查清单_LIVE.xlsx
docker cp nextcloud-nextcloud-1:/var/www/html/data/doro/files/Doro合同审查任务/合同审查清单.xlsx /tmp/合同审查清单_LIVE.xlsx
sudo chown maggie:maggie /tmp/合同审查清单_LIVE.xlsx

# 上传
docker cp /tmp/合同审查清单_LIVE.xlsx nextcloud-nextcloud-1:/var/www/html/data/doro/files/Doro合同审查任务/合同审查清单.xlsx
# 上传
docker cp /tmp/合同审查清单_LIVE.xlsx nextcloud-nextcloud-1:/var/www/html/data/doro/files/Doro合同审查任务/合同审查清单.xlsx
python3 -c "import openpyxl; ws=openpyxl.load_workbook('/tmp/合同审查清单_LIVE.xlsx').active; print(f'NC当前版本: {ws.max_row}行, 最后seq={ws.cell(ws.max_row,1).value}')"

# 上传
docker cp /tmp/合同审查清单_LIVE.xlsx nextcloud-nextcloud-1:/var/www/html/data/doro/files/Doro合同审查任务/合同审查清单.xlsx
docker exec nextcloud-nextcloud-1 chown www-data:www-data /var/www/html/data/doro/files/Doro合同审查任务/合同审查清单.xlsx
docker exec -u www-data nextcloud-nextcloud-1 php occ files:scan --path="doro/files/Doro合同审查任务/合同审查清单.xlsx"

# 上传后验证（write-read-verify，防止覆盖事故）
rm -f /tmp/合同审查清单_VERIFY.xlsx
docker cp nextcloud-nextcloud-1:/var/www/html/data/doro/files/Doro合同审查任务/合同审查清单.xlsx /tmp/合同审查清单_VERIFY.xlsx
python3 -c "import openpyxl; ws=openpyxl.load_workbook('/tmp/合同审查清单_VERIFY.xlsx').active; print(f'上传后验证: {ws.max_row}行, 最后seq={ws.cell(ws.max_row,1).value}')"

# 清OnlyOffice缓存
docker exec nextcloud-onlyoffice-1 bash -c 'rm -rf /var/lib/onlyoffice/documentserver/App_Data/cache/files/data/*'
docker restart nextcloud-onlyoffice-1
```

### 3. 确认并汇报

向Doro确认已标记completed，告知清单已更新到第几条。

### 批量pass后的登记完整性审计（2026-06-15教训）

⚠️ **Doro说\"都pass了\"或\"看下是否都走了pass流程\"时，必须把每一份pass的合同逐一对照 tracker JSON 和 xlsx 两个存储，找出缺漏——不能假设\"workflow跑完了/交付了\"就等于\"登记完整\"。**

2026-06-15教训：6份合同队列全部跑完并交付，Doro说6份都pass。实际核对发现只有2份（端午节、医疗急救招聘）走了完整pass流程，另外4份（金泽蛋糕、赵巷消防、练塘健康积分、徐泾维保）tracker和xlsx**全都没登记**。Doro主动提醒\"excel登记是不够的\"。原因：交付由workflow的final_review完成，但pass流程（写tracker+xlsx）是独立的人工步骤，前面几份漏做了。

**审计脚本逻辑**（用openpyxl+json对照）：
1. 列出本批次所有pass合同的 `delivered_filename`（从任务交付目录或Doro的pass消息提取）
2. 读 xlsx 第E列（文件名列）建一个 set
3. 读 tracker.json 的 `contracts[].delivered_filename` 建一个 set
4. 逐份合同检查：`in xlsx?` + `in tracker?`，打印缺失矩阵
5. 对缺失的合同，**完整补走步骤0（待审查核实+正文读甲方+查重）→ 步骤1（tracker）→ 步骤2（xlsx）**，不能只补一个存储
6. 补完后重新跑一遍审计脚本，确认6份全部 ✅ xlsx + ✅ tracker

**xlsx与tracker必须成对存在**：xlsx是给人看的登记，tracker是给cleanup cron用的。只有xlsx没tracker→文件永远不被自动清理；只有tracker没xlsx→Doro的清单缺条目。审计时两个都要查。

**openpyxl写xlsx的权限陷阱**：从Nextcloud用`sudo cp`下载的xlsx属主是root，openpyxl保存时报`PermissionError`。先`sudo chown maggie:maggie`改属主到当前用户的可写副本再操作。

## 触发模式

Doro会**回复引用**某条交付通知消息并说"pass"。可能是单份也可能批量（"四份都pass"）。
- 引用的消息中包含交付文件名，从中提取合同信息
- 批量pass时逐份处理，每份都写tracker+xlsx

### 审查意见文件的处理
Doro可能把审查意见文件（如"朱家角审查意见-XXX.docx"、"采购协议-审查意见.docx"）也列入pass清单。这些是合同的伴随交付物，**不需要单独的tracker条目和xlsx行**——它们跟随主合同的tracker记录：
- 主合同"【修】XXX.docx" → 写tracker + 写xlsx
- 伴随审查意见文件 → 不写tracker，不写xlsx
- 清理时：审查意见文件跟随主合同一起清理

### Classifier甲方误判的pass处理（2026-06-12教训）
当workflow classifier误判了顾问单位（如把"卫健事业发展中心"误判为"朱家角"），pass流程写tracker/xlsx时必须用**合同正文中的真实甲方名称**，不能用classifier的`our_party_name`。
- 步骤0核对时用python-docx读取合同正文前30段，找甲方全称
- tracker的`party`字段和xlsx的"顾问单位"列写真实甲方

## 清理cron配套信息
- Cron job name: `contract-cleanup`，job_id: `4636467b715d`
- 脚本路径: `~/.hermes/scripts/contract-cleanup.py`
- 调度: 每小时，no_agent静默模式
- 逻辑: 读tracker → 找completed + xlsx_updated_at超24h + cleaned=false → docker exec删Nextcloud待审查/和任务交付/中的文件 → 标记cleaned=true → files:scan
- 安全: 只删tracker中精确记录的文件名，不通配。xlsx在上一级目录碰不到。JSON损坏时静默不操作。原子写入(tempfile+rename)
- **.doc→.docx转换残留（2026-06-11发现+修复）**：原始文件为`.doc`时，workflow会用libreoffice转换为`.docx`副本放在待审查目录。cleanup脚本已有`converted_filename`字段的读取逻辑（第113-116行），但旧workflow跑的合同tracker中缺少此字段导致转换文件残留。**治本修复**（2026-06-11已实施）：在review-contract.yaml的classifier procedure中添加`converted_filename`记录步骤，从classifier→reviewer→editor→deliverer→final_review全链路传递该字段。pass流程写tracker时从workflow输出中提取。cleanup脚本自动生效。
- **手动批量清理（Doro授权模式）**：Doro可能要求按日期批量清除旧文件（不走tracker逻辑），此时按修改时间（stat %Y）过滤，在Nextcloud容器内执行rm，完成后files:scan+清OO缓存。2026-06-11执行过一次：保留6月10日及之后的文件，删除之前的全部（待审查11个+任务交付179个）。

## 已知坑

- **时间窗不能先拍脑袋，必须先用工具确认当天与"上周五"的北京时间边界（2026-07-13 再犯后补强）**：用户要求"查看北京时间上周五及今天，邱律师（查sender id）发了多少合同"时，不能直接按自己理解硬算时间窗，更不能在没交叉验证前就下"周五=0份"结论。必须至少两步核对：①先用 `TZ='Asia/Shanghai' date` 确认当前北京时间与星期；②把北京时间窗口换算成 UTC 后，再同时查 `.meta` 和 `gateway.log`。**只要 gateway.log 已出现 QiuTing 的空消息（msg=''）文件记录，就不能再说该时段 0 份。**

- **查"邱律师发了多少合同"必须双源交叉，不可只靠 cache meta（2026-07-13 教训）**：仅查 `~/.hermes/cache/documents/*.meta` 可能漏掉周五文件或误判为 0。标准做法：
  1. `.meta`：按 `sender_id == QiuTing` + 北京时间窗口换算后的 UTC 区间统计
  2. `gateway.log`：按同一时间窗 grep `platform=wecom user=QiuTing chat=QiuTing msg=''`，这是文件消息的一手证据
  3. tracker + xlsx：核对这些文件是否都 workflow 完成、是否都 pass、是否都登记 excel
  4. **没有把两源对齐前，不得下"都完成了/没有遗漏"结论**

- **被 Doro 说"胡说八道，重新查，查明情况再说"时，表示你刚才的结论缺乏证据链**：此时必须回退到原始数据源重查，不得只改措辞继续硬答。正确做法：先承认上一条没查明，再用不同数据源（如从 meta 转到 gateway.log）交叉验证。
- **审查workflow交付物时必须区分原文自带修订 vs WB修订（2026-07-13 Doro指示）**：Doro要求"对照原文件，WB-1如果是原文自带修订，保持不动。你只需要看workflow是不是准确遵守了workflow的规则，包括命名规则"。具体方法：①用zipfile读原文件（待审查目录）的revision authors，确认哪些author是原文已有的 ②读交付件，按author过滤只看WB的修订 ③逐条核对WB修订是否符合审查规则 ④原文自带的修订（其他author）不评价、不报告为问题。**常见误判**：把原文WB-1的修订当成workflow的WB修订来审查——必须先确认author再下结论。

- **核查汇报必须先用工具验证再说（2026-07-01+07-02 升级为顶部红线）**：详见本skill顶部「红线：验证指令 ≠ 回忆指令」章节 + `references/audit-methodology.md`。2026-07-02 同一轮对话中此规则被违反4次，导致Doro信任破裂（"到了无法信任你的程度"）。不再重复列举——执行时加载顶部红线即可。

- **"确定吗？再仔细查实"（2026-07-02 追加）**：Doro说"确定吗""再仔细认真查实"时，意思是**上一条回复的结论不够可信/不够深入**。正确做法：不要只是重复上一轮的输出，而是用不同角度/更深层的工具去交叉验证。具体：①查session_search看是否有其他session做过相同操作 ②查uwf step list确认每个thread走到了哪一步 ③确认通知确实被发出（session中有_send_wecom的tool call记录）而非假设。核心原则：**每一条"确认X发生了"的结论，都必须有对应的工具调用输出作为证据。**

- **文件"消失"的排查思路（2026-07-01 教训）**：Doro发现待审查/交付目录文件不在时，不要急着说"被误删"。先排查：①tracker 的 `cleaned` 字段是否已为true（cron清理了）→ 不可能不到24h；②auto_notify 是否正常工作（文件可能从来没上传到待审查目录，workflow直接从cache路径读文件完成审查，但Doro在Nextcloud看不到）；③是否是手动操作中 `docker exec rm` 删了。根因很可能是"从来没上传到待审查"而不是"上传后被删了"。

- **交付件被 Doro 手动编辑后大小/内容会变 —— pass 时不覆盖、只登记（2026-06-23 实证）**：Doro 收到交付件后可能自己在 OnlyOffice 里编辑（删掉部分批注内容、改条款等），导致 Nextcloud 上的交付文件大小/字节/md5 与 workflow 原始交付件不同；且 OnlyOffice 后台处理期间文件**大小会持续变化**，docker cp 出来用 pymupdf/python-docx 读可能是 0 页/0 批注的中间态。**看到交付件与你核验时不一致，先确认是不是 Doro 自己改的，绝不要当成文件损坏去"修复"或用本地完整版覆盖。** Doro 说 pass 后：以 Doro 改后的版本为准，只写 tracker+xlsx 登记，**不触碰交付目录里的文件**。2026-06-23 教训：交付 PDF 从 20MB 变 3.5MB 且持续变化，我误判损坏、准备用本地版覆盖，Doro 说"是我删除了一些内容，你直接做 pass 就可以"。

- **PDF 扫描件合同走批注模式，不 OCR 转 docx（2026-06-23 Doro 明确）**：收到扫描件 PDF 合同（无文字层）时，**不要纠结"OCR 转 docx 才能修订"**——workflow 对 PDF 用**批注模式**审查（高亮 Highlight + 批注气泡 Text 配对，author=WB），原文零污染，这是正常流程（seq=201 朱家角.pdf、seq=207 阳澄湖团建.pdf 都是 PDF 批注交付）。Doro 原话"PDF 的修改使用批注，这是 workflow 的正常流程"。PDF 交付件**核验/pass 时**：核批注数/author/高亮锚定位置/原文页数不变，**不套用** INS/DEL/字号/numPr 那套 docx 专项检查（final_review 对 PDF 会自动判定这些不适用）。

- **交付文件位置：根目录 `任务交付/` 是正确的（2026-06-29 确认）**：workflow deliverer 明确写着上传到 `Doro合同审查任务/任务交付/`（根目录）。不要把文件移到顾问单位子文件夹——那里不是交付目的地。pass 流程步骤0核对时，交付文件应该在根目录 `任务交付/` 中。如果 deliverer 同时上传到了子文件夹（如 final_review 越权重复上传），子文件夹里的是多余的，应清理。

- **final_review 越权重复上传（2026-06-29 教训）**：workflow 中上传是 deliverer 的职责，final_review 只负责质量检查和通知 Doro。但 LLM 执行 final_review 时可能越权上传文件（到子文件夹而非根目录 `任务交付/`），且使用完全不同的命名格式。**判别**：`stat` 比较文件大小，同内容两份不同名=重复。**处理**：保留 deliverer 上传的（【修】/【审】+ 原始文件名），删除 final_review 额外上传的。**根因**：review-contract.yaml 的 final_review procedure 需修改，明确禁止上传动作（待 WeiWei 实施）。

- **审查意见标题空壳（2026-06-29 教训）**：workflow editor 生成的审查意见文档标题是"关于《合同》的审查意见"——"《合同》"是占位符，没有替换为实际合同名称。classifier 已正确提取了 `contract_title`，但 editor 生成审查意见时没有填入。**pass 流程必须检查审查意见标题**：如果标题是"关于《合同》的审查意见"，需要手动修正为"关于《{合同实际名称}》的审查意见"。修正方法：用 zipfile + lxml 读 document.xml，找到标题段落的所有 `w:t` 节点，第一个设为完整标题，其余清空。

- **【审】审查意见文件缺失（2026-06-30 发现）**：deliverer 有时只上传【修】修订版而没有上传【审】审查意见文件。**排查**：`sudo find .../任务交付/ -name "【审】*"` 检查是否有对应的审查意见。**处理**：如果缺失，需要手动生成或报告Doro确认是否需要补做。审查意见是companion文件，不影响主合同的pass流程（不需要tracker/xlsx条目），但Doro可能期望看到。
- **Workflow常见格式缺陷清单（2026-07-13 审计总结，详见 `references/pre-pass-audit-checklist-20260713.md`）**：
  1. WB INS rFonts多余属性（hAnsi/cs/hint）——每份都有，每次审计必查必清
  2. 新增条款标题pStyle错误（用了Style15而非Heading4等）
  3. 新增标题带空格（"第七条 转包"应为"第七条转包"）
  4. 子编号未顺延（只改了章编号，没改内部X.Y编号）
  5. 赔偿上限遗漏（双向条款的20%上限未删）
  6. 保密存续重复插入（原文已有同义表述）
  7. 脚注未用修订格式

- **同模板合同修订一致性（2026-07-03 Doro确认：手动调整）**：不同顾问单位提交审查的合同若为相同模板，修订需保持一致（规则9已有）。但不改workflow自动化逻辑——Doro指出不一致时手动调整即可，避免矫枉过正。不需要template_type标签或修订库。

- **非合同文件进入workflow审查（2026-07-01 香花桥招标需求实证）**：classifier识别出"招标需求文件"但仍走了完整review+edit流程并交付了修订版。Doro判定此类文件无需审查，直接通知邱律师。**已修auto_notify加L1文件名预筛**（关键词：招标需求/技术方案/报价单等）。但classifier兜底层未修——仍然会把非合同当合同审。根因：workflow的routing graph没有从classifier直接到$END的non-contract路径。**pass前排查**：如果交付物对应的原始文件明显不是合同（招标需求/投标文件/会议纪要等），直接删除交付物并通知邱律师，不做pass。

- **审查意见文件是错误交付物（2026-07-01 发现）**：workflow 有时会生成不该有的审查意见文件（如合同已被其他thread审查过、或classifier误判导致多生成）。**pass前排查**：检查任务交付目录中是否有不属于本次审查的【审】文件。如果是错误交付物（如旧版残留、重复审查产物），直接删除不pass。判断标准：审查意见的标题和内容是否对应本次审查的合同，标题是否是"关于《合同》的审查意见"（占位符）。
- **cleanup脚本不处理tracker顶层"ready"条目 + 不清理本地目录（2026-07-03 查明根因）**：`contract-cleanup.py` 只遍历 `tracker["contracts"]` 数组中 `status=completed` + `cleaned=False` 的条目。但以下两类文件不会被清理：
  1. **Tracker顶层字典键**（status=ready的条目）：这些是workflow跑完但尚未被Doro pass的合同，存储在tracker的顶层键而非contracts数组里。cleanup脚本看不到它们。Doro pass后，pass流程会把它们从顶层搬到contracts数组并标记completed——此时cleanup才能处理。**如果pass流程有bug没搬进contracts数组，文件永远不会被清理。**
  2. **本地 `~/.hermes/shared/Doro合同审查任务/待审查/` 目录**：cleanup脚本只删Nextcloud容器内的文件（`docker exec rm`），本地shared目录的副本无人管理。这些是auto_notify上传NC后留下的本地副本——NC侧被cleanup清了，本地的永远残留。
  - **诊断方法**：`ls ~/.hermes/shared/Doro合同审查任务/待审查/` 如果有文件，且对应的tracker条目已经completed/ready，就是此bug。
  - **临时清理**：确认文件在tracker中已completed后，手动 `rm` 本地副本即可。
  - **根治**：cleanup脚本需要增加两段逻辑：①遍历tracker顶层ready条目（Doro已pass但pass流程漏搬的）②清理本地shared/待审查/中已completed的文件。

- **沟通时间统一使用北京时间（2026-07-03 Doro多次纠正）**：所有对外沟通（包括向Doro汇报workflow状态、文件接收时间等）一律使用北京时间。服务器UTC时间仅在内部日志/脚本中使用，不对用户展示。

- **Doro说"查清楚"/"你去查原因"——必须用工具验证后再给结论**：Doro要求查证时，不能凭记忆或context summary回答。必须实际调用工具（session_search、terminal读文件/目录、tracker等）取得证据后再汇报。2026-07-13教训：被要求"查清楚我哪些说了pass"时，需要在本session对话记录中找到Doro的原话，不能凭印象列清单。

- **脚注"法律顾问修订版"必须用修订格式（2026-07-13 Doro纠正）**：添加页脚文字时，必须包裹在`w:ins`元素中（author=WB），不能作为普通文本直接写入。OnlyOffice/Word中显示为带修订标记的新增内容。实现方式：用python-docx添加footer文本后，再用zipfile+lxml找到footer XML中的对应run，包裹进`w:ins`元素。

- **交付件字体大小不一致（2026-07-01 Doro纠正，根因细化）**：有两种常见成因：
  1. **段内混用**：同一段落内不同run使用不同字号（如sz=21和sz=24混用）。修复：找dominant size统一。
  2. **INS-only新段落缺sz**（更隐蔽）：`add_clause`插入的全新段落，原文docDefaults无sz定义或sz≠邻居段落的显式sz。新段落INS run不写sz→继承docDefaults→与显式sz=24的邻居段落不一致。**诊断**：找所有INS-only段落（整段只有w:ins），检查其run的rPr是否有sz，再对比前后段落的sz。缺sz且邻居有显式sz=补上。
  3. **numPr叠加**：加了手动编号但没去掉原自动编号→显示"1. 第一条"。修法见contract-editor skill的"strip numPr"规则。

- **deliverer 重复上传（同内容两份不同名）（2026-06-29 教训）**：deliverer 可能对同一合同上传两次，文件大小完全一致说明内容相同。**判别**：`stat` 比较文件大小。**处理**：保留命名规范的那份（【修】/【审】+ 原始文件名），删除另一份。

- **原文件自带【修】前缀时命名错误（2026-07-13 璞石合同实证）**：邱律师发来的文件本身已有【修】前缀（如`【修】练塘-硬件购销合同-璞石医疗2026.7.13.wps`），workflow按规则应生成`【修】【修】练塘-...docx`（原文件名一字不动，前面再加【修】），但实际只生成了`【修】练塘-...docx`（吞掉了原文的【修】前缀）。**审查时判别**：原文件名含【修】时，交付文件名应出现两个【修】。只有一个=workflow命名错误。**根因**：workflow的命名逻辑strip了原文件名中已有的【修】前缀后再加自己的。

- **Cache hash前缀混入交付文件名（2026-06-30 实证）**：deliverer 有时把 cache 文件名直接当交付文件名上传，产生类似 `【修】doc_0ad4ee63bff5_印刷品制作合同2026.6(1).docx` 的文件名。**判别**：文件名含 `doc_[a-f0-9]{12}_` 模式。**处理**：删除带 hash 前缀的那份，保留正确命名的（`【修】印刷品制作合同2026.6(1).docx`）。**根因**：deliverer 从 cache 复制文件时没有去掉 cache hash 前缀重命名。

- **companion 文件（审查意见/流程单）不会被 cleanup 自动清理 → 永久残留孤儿（2026-06-21/29 实证）**：`contract-cleanup.py` 只删 tracker 里 `original_filename`/`converted_filename`/`delivered_filename` 三个字段精确匹配的文件。companion 从不进 tracker → 主合同被清理后 companion 永远残留。**排查**：运行 `references/cleanup-audit.py` 审计脚本，列出所有 not-in-tracker 的文件。**应急清理（Doro 授权后）**：
  ```bash
  # 必须搜索所有任务交付路径（根目录+顾问单位子目录都可能有）
  sudo docker exec nextcloud-nextcloud-1 find /var/www/html/data/doro/files/Doro合同审查任务/ -path "*任务交付*" -name "*审*意见*"
  # 确认后逐个删除
  sudo docker exec nextcloud-nextcloud-1 rm -f "<path1>" "<path2>" ...
  # 扫描+清缓存
  sudo docker exec -u www-data nextcloud-nextcloud-1 php occ files:scan doro --path="/doro/files/Doro合同审查任务/"
  docker exec nextcloud-onlyoffice-1 bash -c 'rm -rf /var/lib/onlyoffice/documentserver/App_Data/cache/files/data/*'
  docker restart nextcloud-onlyoffice-1
  ```
  ⚠️ Companion 可能同时存在于根目录 `任务交付/` 和顾问单位子目录（如 `朱家角镇社区卫生服务中心/任务交付/`）——find 命令用 `-path "*任务交付*"` 通配搜索确保不遗漏（2026-07-06 实证：肃言/恭兴审查意见各在两个位置共4份）。
  **治本方案见 `references/companion-cleanup-proposal.md`**，需 WeiWei 决策后实施。
  
  - **Companion 判定不能靠想当然命名（2026-07-13 白鹤劳务派遣协议教训）**：Doro说“交付文件夹里的合同及companion全部做pass”时，**先查清交付文件夹里到底有哪些相关文件，再决定有没有 companion**。不要因为很多合同通常会带审查意见，就默认本案也有；也不要只看主文件名一次就下结论。正确顺序：①先在 `任务交付/` 中做**宽匹配扫描**（合同名关键词、当事人名关键词、业务关键词都要扫）②再把整个 `Doro合同审查任务/` 目录补扫一遍，确认没有藏在别的子目录里的 companion ③最后再对 tracker+xlsx 核对是否已有 completed 记录。**如果实扫结果只有主合同一份，就要明确汇报“合同1份、companion 0份”，而不是笼统说“都pass了”。**
- **Companion 命名规则（2026-07-01 Doro确认）**：交付物=`【修】{原文件名}`，companion=`【审】{原文件名去扩展名} 审查意见.docx`。pass skill 扫描时按此规则拼确定性文件名+`nc_file_exists()`查询，查到就写入 tracker 的 `companion_files` 字段。cleanup 脚本读 `companion_files` 一并删除。两头（pass skill + cleanup script）必须同步改才有效果。workflow editor/deliverer 生成审查意见时也必须强制用此命名规则。

- **手动启动workflow前必须查实auto_notify是否已处理（2026-07-03 教训）**：发现cache中有新文件时，不要直接`uwf thread start`。必须先：①查`/tmp/auto_notify_new_file.log`确认auto_notify是否已检测到并启动了workflow ②`uwf thread list | grep running\|idle`看是否已有对应thread在跑。auto_notify正常工作时会自动上传NC+启动queue runner+排队执行，手动启动会导致重复审查。2026-07-03实证：邱律师发了文件，auto_notify 30秒内就启动了workflow（thread 06FJDADW），我没查就又手动启动了第三个重复thread，被Doro纠正。

- **不判断文件是否相同/重复（2026-07-03 Doro纠正）**：邱律师发了几次、发什么文件，不是我该判断的。不要说"同上""同名文件""是重复的"——即使md5一致也不做这个判断。auto_notify和workflow自己处理，我不干涉。待审查目录里有几份就是几份，workflow跑几个就是几个。

- **沟通必须使用北京时间（多次纠正）**：所有与Doro/Maggie的时间沟通一律用北京时间，包括表格、汇报、日志引用。服务器是UTC，必须+8转换后再输出。不写UTC时间。

- **"删掉批注"指令——先验证再操作（2026-07-08）**：Doro可能要求"删掉批注，提交后做pass"。操作前必须先用zipfile检查comments.xml是否存在+commentRangeStart数量。如果文件无批注（comments.xml不存在且无comment引用元素），直接汇报"文件无批注"然后继续后续操作（提交/pass），不需要强行"删除"不存在的东西。

- **邱律师同名文件不自行判断是否重复（2026-07-03+07-09 加强）**：邱律师发的多次同名文件，不要自己判断"重复发送"。同名文件可能：①不同顾问单位②修改版（字节不同=内容变了）③同一份发了两次。**只要字节大小不同，就必须视为不同合同独立审查**。2026-07-09教训：字节差407的两份合同有3处实质性条款差异（项目名称、支付方式、合同期限），不是"重复发送"。正确做法：检查字节是否相同→不同→独立审查或私信邱律师确认。

- **交付件大小/内容突变，先确认是不是用户手动编辑了，别当损坏（2026-06-22 实证）**：pass 前看到 Nextcloud 上的交付 docx/pdf 大小骤变（如 20MB→3.5MB 还在持续变、md5 对不上本地核验版），第一反应**别**判为"文件损坏/被进程写坏"。先确认是不是 Doro 自己在 OnlyOffice 里改了（删批注、调内容）。确认是用户编辑后：**不覆盖、不动交付目录的文件，以用户改后版本为准**，直接走 pass 登记（tracker+xlsx）。**铁律：pass 登记只动台账（tracker/xlsx），交付目录里的成品文件除非用户明确要求重做，否则只读不写。**

- **`.doc` 原始文件残留（2026-06-29 实证）**：tracker 的 `original_filename` 有时记录了 `.docx`（转换后文件名）而非 `.doc`（真正的原始文件），cleanup 按 `.docx` 去删找不到 `.doc` → 残留。**排查**：cleanup-audit.py 会标记为 NOT_IN_TRACKER。**预防**：pass 写 tracker 时确保 `original_filename` 是邱律师发来的真实文件名（含扩展名），不要写转换后的文件名。

- **编号"错误"误判——markup视图双编号是正常现象（2026-06-16教训）**：OnlyOffice/Word修订视图(markup)下，自动编号列表项被删除（含他人如屠佳青的删除）或新增条款插入后，后续项会显示"（3）（2）""（4）（3）"这类双编号——前一个是删除/插入前的旧编号，后一个是接受修订后的新编号。这是track changes的正常渲染，**不是编号错误**。Doro/Maggie说某份合同"编号修改错误"时，先别急着改：
  1. 用execute_code生成"接受所有修订后"的版本：删除所有`w:del`元素 + 删除带段落标记删除(`pPr/rPr/del`)的整段 + 解包所有`w:ins`（把ins的子元素提到父级再删ins壳）
  2. 用OnlyOffice x2t渲染accept版PDF，pdftotext看**最终编号**是否连续正确
  3. 若accept后编号正确 → workflow没改错，markup双编号是正常的，**恢复workflow原版即可，不要擅改**
  4. 若要改，必须先问清Doro/Maggie指的是accept后哪一处，他人(屠佳青等)的修订按铁律不能动
  - 2026-06-16教训：把端午节合同（转包6误改成7）、医疗急救招聘合同的正常编号重排误判为错误并擅自改动，被Maggie两次纠正"workflow改得都没错，恢复成workflow修订版本"。x2t accept渲染命令见本skill的"用OnlyOffice渲染自查"或contract-editor skill。

  - **但确有真编号重复时要修（2026-06-15端午节实测，与上条不冲突）**：自动编号列表项（numbering.xml中`numId`对应的`abstractNum`有`start=N`、`lvlText=%1、`）渲染出的编号，与后续**手动键入数字**的tracked插入条款（`w:ins`里`w:t`文本直接以"6、""7、"开头）会真冲突。本案"售后服务"是auto-number（numId=3, start=6 → 渲染为6），紧跟的WB插入条款手动写了"6、转包限制" → 出现两个6。这是**真错误**，不是markup双编号假象。判别要点：
    1. 假象（不改）：同一条款同时显示两个编号"（3）（2）"，是track changes接受前后的新旧编号叠加 → accept后渲染连续就别动
    2. 真错（要改）：两个**不同条款**各自显示同一个数字（售后服务=6、转包限制=6）→ accept后仍重复 → 必须修
    3. 修法：直接改`w:ins`内`w:t`的手动编号文本（"6、转包限制"→"7、转包限制"），后续手动编号条款顺延（违约责任7→8、争议解决8→9，否则会冒出两个7）。用zipfile读document.xml→字符串replace（先`assert xml.count(old)==1`确认唯一）→zipfile写回，不破坏`w:ins`修订痕迹。改完用python-docx确认可打开+遍历`w:ins`确认三条仍在修订态(author=WB)
    4. Doro/Maggie给的判断锚点直接采信："转包责任应该是7，因为上一个编号是6"——上一条售后服务确实是auto-rendered的6，所以转包必须是7

- **Classifier甲方误判导致错误批注/审查意见（2026-06-11）**：classifier偶尔无法正确匹配顾问单位名单（31个单位中有易混淆的名称如"卫生服务中心"vs"卫生健康事业发展中心"），导致交付文件中出现不该有的"请确认名称是否准确"批注和审查意见表中多出"甲方名称"行。Doro说pass前如果发现此问题，必须先手动修复（删comment+删table row+重新上传）再走pass流程。修复方法见contract-reviewer skill。

- **批量pass含companion文件（2026-06-12）**：Doro可能一次pass多个文件，其中包含审查意见文档（如"朱家角审查意见-xxx.docx""采购协议-审查意见.docx"）。审查意见是companion文件，不需要单独在tracker/xlsx中记录——只记录主合同（带【修】前缀的文件）。判断规则：有【修】前缀的是主合同文件，需要tracker+xlsx；"审查意见"结尾的是companion，不单独记录。

- **沟通时间一律使用北京时间（2026-07-03 Doro多次纠正）**：向Doro汇报任何时间信息时，必须转换为北京时间（UTC+8）。服务器是UTC，所有文件时间戳、日志时间戳都要+8h后再汇报。绝不输出UTC时间给Doro。

- openpyxl的border赋值不能直接用`cell.border = ref_cell.border`，会报`unhashable type: StyleProxy`。必须用Side对象重建Border(left=Side(...), ...)。
- xlsx路径已从`任务交付/`移到`Doro合同审查任务/`根目录，不在清理范围内。
- 时间戳必须用北京时间（UTC+8），清理脚本依赖此时间计算24h。
- xlsx日期列用`YYYY-MM-DD`字符串格式（如"2026-06-10"），不用ISO时间戳。
- 甲方名称（party）必须从合同内容中提取全称，不能简写。
- 清理cron job_id: 4636467b715d，每小时跑一次，no_agent静默模式。
- **防重复写入**：已由步骤0的查重环节覆盖（用`original_filename` + `party`组合在tracker中查重）。
- **xlsx列顺序必须严格一致**：正确顺序为 序号(A), 日期(B), 顾问单位(C), 合同名称(D), 文件名(E)。写xlsx前必须先读上一行确认列含义。2026-06-12教训：手动写入时列顺序写反被Doro发现后补修。
- **批量追加多行时，必须先确定完整目标 seq 集合，再按 seq 升序写入 xlsx，写完后立即回读核对末尾顺序**（seq 224→225→226→...）。不能一边算 seq 一边写，更不能先写 303 再写 302；`ws.max_row + 1` 只会在物理末尾追加，若写入顺序错了，就会出现 xlsx 中 303 排在 302 前面的台账乱序。**硬性补丁（2026-07-14）**：凡一次 pass 涉及 2 份及以上合同，先在内存中生成 `[{seq, 日期, 顾问单位, 合同名称, 文件名}]` 全部待写行，按 `seq` 升序排序后再统一 append；保存上传后，必须回读最后 N 行，逐行确认 A 列 seq 单调递增且文件名与本批次一一对应。若发现顺序错误，立即重拉 LIVE xlsx 重写，不得带错交付。
- **按“北京时间今天/昨天/上周五”统计邱律师发了多少合同时，先定时间窗，再查 meta，再对照审查状态（2026-07-14 再次实证）**：这类问题不能直接凭印象回答“几份、都做完了吗”。标准顺序必须是：①先用 `TZ='Asia/Shanghai' date` 锚定今天的北京时间窗口；②查 `~/.hermes/cache/documents/*.meta` 中 `sender_id=QiuTing` 且落在该窗口内的文件，得到**今天实际收到的文件清单**；③再去 tracker 中逐份核对这些文件对应的 `status/seq/delivered_filename/xlsx_updated_at`；④最后才汇总结论。**重点**：meta 统计的是“今天收到几份”，tracker 统计的是“这些文件审查到了哪一步”，两者不能互相替代。
- **同名不同来源/不同顾问单位的合同，统计和汇报时必须拆开，不得混成一句“劳务派遣协议已完成”（2026-07-14）**：如果今天收到的是 `劳务派遣协议.doc`，但 tracker/任务交付里同时还存在 `【修】白鹤--劳务派遣协议.docx` 等近名文件，汇报时必须明确区分“今天收到的这份”与“其他同类/同模板/同主题文件”。不能把别的合同的 completed 记录拿来替代今天这份的审查状态。标准动作：按 `original_filename` 精确对 tracker 命中，再补充说明是否另有近名已完成文件。
- **被用户要求“再核查一次”时，必须升级为四路交叉核查，不得只复述上一轮结论（2026-07-14）**：对“是不是同名合同但顾问单位不同”“今天收到的到底是哪几份”这类追问，重查时至少同时核：①Nextcloud 全目录文件扫描（不只根目录任务交付）②tracker 命中记录 ③xlsx 命中记录 ④cache meta/原件名称与正文中的甲方或项目编号。回复必须把四路结果并列写出，再下结论。不能只说“我刚才已经看过了，结论不变”。
- **自动 cleanup 依赖 tracker 文件名与 Nextcloud 实际文件名精确一致（2026-07-14）**：若发现“历史台账文件名”和“当前 NC 实际文件名”不一致，即使该条记录已经 completed，仍要同步修正 `tracker.original_filename` / `tracker.delivered_filename`，必要时同步修正 xlsx 第 E 列文件名，确保三者一致：①tracker ②xlsx ③NC 实际文件。否则 cleanup 按精确文件名匹配时会漏删或误判。修正后必须再次核对目标待审查文件与任务交付文件在 NC 中确实存在。
- **用户只要结果，不要过程解释时，汇报必须收敛成一句结论（2026-07-14 Doro纠正）**：当用户明确说“我就要一个结果”时，禁止继续附加过程说明、背景铺垫、风险解释或“补一句严谨的”。此类场景只回复最终结论，例如“能。”、“已完成。”、“不能，需要X条件。”；如确需补充，等用户追问后再展开。
- **统计“今天收到的合同”时，先答收到清单，再答审查状态，别把两件事揉成一句笼统结论（2026-07-14）**：推荐输出结构固定为：①今天收到几份；②逐份列出收到时间；③逐份列出审查状态（未启动 / 审查中 / delivered / completed）；④如有同主题近名文件，单列“另有近名已完成文件，不等于今天这份”。这样可以避免把“收到数量”和“已完成数量”说串。
- **乙方空白时的处理**：合同乙方名称空白→不问Doro"该问谁"，直接查文件来源（cache/documents时间戳+session_search找发送人），确定是邱律师发的就直接私信邱律师询问。Doro可能说"邱律师回复了就直接补上 我不管了"——意思是邱律师回复后自主完成tracker+xlsx更新，不再找Doro确认。
- **私信邱律师的send_message路由问题**（2026-06-12）：`send_message(target='wecom:QiuTing')`会路由到home channel而非QiuTing私信。必须用`_send_wecom(extra, 'QiuTing', msg)`直接发送才能到私信。
- **私信任意企微用户的首选方法 = `wecom_dm.py` 脚本（2026-06-21 再犯后确立）**：`send_message(target='wecom:<user>')` 对企微用户名会**静默回退到 home channel**（JiaQian），既发不到目标、又可能违反信息隔离（把 Doro 团队内容发进 Maggie 渠道）。`_send_wecom(extra, ...)` 需要 gateway 的 `extra` 上下文，在 terminal/execute_code 里拿不到。**最稳的通用方法**是独立脚本：`python3 ~/.hermes/scripts/wecom_dm.py --to <别名> --text "内容"`——自己开 WebSocket + `aibot_send_msg` + `chat_type=1` 发主动私信，永不串号、目标唯一确定。白名单别名：doro / jiaqian(贾茜Maggie) / qiuting(邱律师) / weiwei(技术支持) / shasha(苌莎莎) / yangayi(颜伽艺) / xiaonan。先 `wecom_dm.py --list` 核对别名→userid 再发。**铁律：私信非 home 的企微用户，一律走 `wecom_dm.py`，绝不用 `send_message(target='wecom:X')`。** 2026-06-21 教训：给魏玮发技术讨论用了 `send_message(target='wecom:WeiWei')`，静默落到 JiaQian home channel，既没到魏玮又把 Doro 团队的脚本细节泄露进 Maggie 渠道，改用 `wecom_dm.py --to WeiWei` 才真正送达。
- **不干涉workflow铁律（2026-07-03 Doro纠正）**：发现邱律师发了新文件时，**禁止直接手动启动workflow**。必须先查 `/tmp/auto_notify_new_file.log` 和 `uwf thread list` 确认auto_notify是否已经处理。不判断文件是否重复（"你也不要去判断是不是同一份文件"）——让系统自己处理。2026-07-03教训：auto_notify已正常工作并启动了workflow，我没查就手动又启动了一个重复的。
- **手动启动workflow前必须查实auto_notify是否已处理（2026-07-03 Doro纠正）**：发现邱律师发了新合同时，**第一步不是手动启动workflow**，而是：①查`/tmp/auto_notify_new_file.log`确认auto_notify是否已检测并处理 ②查`uwf thread list | grep running\|idle`确认是否已有对应thread在跑。只有确认auto_notify没处理（日志无记录+无相关thread）才手动启动。2026-07-03教训：auto_notify已正常触发并启动了workflow，我没查就又手动启动了一个重复的。**"收到即启动"的前提是"确认没有被自动处理"。**

- **不判断邱律师发的文件是否重复/相同（2026-07-03 Doro纠正，2026-07-08 再次验证）**：邱律师多次发送同名文件时，不要自行判断"是同一份发重了"。存在同文件名但不同版本、不同条款内容的可能性。2026-07-08实证：同日发两份"2026年华新镇公立中小学生健康体检服务合同.docx"（11:05和16:04），文件大小不同（27889 vs 27482 bytes），实际条款差异显著（项目内容、付款方式、合同起始日均不同）——完全是两份实质不同的合同版本。queue-runner因同名文件已在done/直接SKIP导致第二份没审查。**核查方法**：用python比对两文件文本差异（zipfile提取全文+difflib对比），有实质差异则为不同合同/不同版本，须独立审查。**报告时绝不说"重复发送"**——Doro问"如何判断是重复发送"即提醒不该自行判断。

- **恢复文件到workflow原始交付状态（2026-07-13 实证）**：Doro要求撤销手动修改、恢复到workflow交付版本时，三个来源按优先级尝试：①NC版本历史（`files_versions/`目录下的`.vXXXXXXXXXX`文件，时间戳最早的=workflow首次上传版本）②`/tmp/pass_check_*`文件（之前做pass检查时从NC拉取的快照，如果当时还没手动修改则=workflow版本）③重新跑workflow（最后手段）。恢复后必须`docker cp`回NC + `chown www-data` + `occ files:scan`。

- **Tracker重复条目（同一合同不同文件名）**：workflow跑多轮或auto_notify重复触发时，tracker可能出现同一合同的多条delivered记录（文件名带版本后缀如`_v0113.doc`vs不带的）。批量pass时需注意：只pass实际在任务交付目录中的那份（【修】前缀的），旧的重复delivered条目如果文件名与NC上的交付物不匹配，不影响pass流程但会残留在tracker中（cleanup会因找不到文件而跳过）。

- **手动启动前必须先验证auto_notify是否已处理（2026-07-03铁律）**：发现邱律师新文件后，**禁止**直接手动启动workflow。必须先：①查`/tmp/auto_notify_new_file.log`确认auto_notify是否已检测并处理该文件 ②`uwf thread list | grep running`确认是否已有workflow在跑。只有确认auto_notify确实没工作（进程死亡或模式B静默失效）才手动介入。2026-07-03教训：auto_notify已正常触发workflow，但我没查就手动又启动了一个重复的，被Doro纠正"不要去干涉workflow"。**邱律师发多次同名文件不自行判断是否相同**——让系统处理，同时私信邱律师确认。

- **交付通知丢失的诊断与补发（2026-07-09 实证）**：Doro说"有几份合同我没有收到交付通知"时，按以下流程排查：①检查tracker中status=delivered的记录 ②查gateway.log在对应delivered_at时间前后是否有846609/Timeout/WS closed错误 ③确认是WS断连导致fire-and-forget通知丢失 ④用`wecom_dm.py --to doro`补发。**根因是企微WS凌晨不稳定+通知无重试机制，详见 `references/notification-ws-failure-pattern-20260709.md`。** 临时止血=手动补发；根治=watchdog增加notification_sent检查和补发逻辑（方案待实施）。

- **通知静默丢失——workflow完成但Doro没收到通知（2026-07-09 实证）**：workflow全流程跑完（thread status=end, tracker=delivered），但Doro说没收到交付通知。根因：`final_review`步骤通过`_send_wecom`发通知时，企微WebSocket已断连（errcode 846609: aibot websocket not subscribed），发送静默失败，无重试机制。**症状**：Doro说"没收到通知" + tracker有delivered记录 + `grep '846609' ~/.hermes/logs/gateway.log`在final_review执行时间段有报错。**诊断**：①查tracker找delivered_at时间 ②查gateway.log该时间±5分钟有无846609/WebSocket error ③确认final_review步骤确实跑完了（`uwf step list <thread>`有final_review且thread status=end）。**补救**：用`python3 ~/.hermes/scripts/wecom_dm.py --to doro --text "合同审查完成通知（补发）:..."`手动补发。**预防**：目前无自动重试机制。如果发现当天有WebSocket中断记录，主动检查该时段内完成的所有workflow是否通知成功——以gateway.log中有对应的"Sending response...to doro"记录（不含后续846609 error）为准。

- **"delivered但Doro没收到通知"诊断（2026-07-09 实证）**：Doro说"有几份没收到交付通知"时，根因通常是final_review发通知时WebSocket已断（846609错误）。Workflow全程跑完（classifier→reviewer→editor→reviewer→deliverer→final_review），queue-runner标记delivered，但final_review内的`_send_wecom`因WS断连静默失败。**诊断步骤**：①tracker找status=delivered的合同 ②gateway.log搜846609和"WebSocket error"确认断连时段 ③`uwf step list <thread_id>`确认final_review确实执行过（有时长=执行了） ④确认交付文件在任务交付目录存在。**补救**：用`wecom_dm.py --to doro --text "合同审查完成通知（补发）:..."`补发。补发内容含：合同名、顾问单位、修订数（ins/del计数）、主要修订摘要。注明"因XX时段企微WebSocket中断未实时送达，现补发"。

- **Watchdog "suspended + already delivered" 死循环（2026-07-08 实证）**：当 workflow 在 `final_review` 阶段因 HTTP 500 suspended，但 deliverer 已经成功交付（tracker=delivered）时，watchdog 会无限循环 BLOCK resume 且不归档文件，同时阻止 runner 重启。表现：Doro 没收到通知但文件已在任务交付目录。**诊断**：`tail /tmp/contract-queue/watchdog.log | grep "BLOCK resume"`。**止血三步**：①`uwf thread cancel <thread_id>` 取消所有suspended thread ②`mv /tmp/contract-queue/<files> /tmp/contract-queue/done/` 清空queue ③对已delivered的合同直接做pass（tracker delivered→completed + xlsx追加）。**注意**：可能影响一批合同（2026-07-08是5份同时卡住），需全部处理。详见 `references/watchdog-suspended-delivered-deadlock-20260708.md`。

- **同名文件判重铁律（2026-07-08 华新镇体检合同教训，Doro明确要求）**：判断两份合同是否为"同一份"，**绝不能只看文件名**。邱律师经常对同一份合同发送修改版（文件名不变但内容已改），也可能不同顾问单位使用相同文件名。**判断标准——必须比对合同实质内容**：
  1. 甲方（顾问单位）名称
  2. 金额/费用条款（总价、单价、费用上限等关键数字）
  3. 合同期限（起止日期）
  4. 项目内容描述
  5. 字节大小
  
  **以上任何一项不同 → 不同合同/新版本，必须独立审查。全部相同 → 重复发送，可跳过。**
  
  此规则适用于所有环节：auto_notify入队、queue-runner启动前、手动操作、以及任何需要判断"是否已审查过"的场景。auto_notify已通过 `~/.hermes/scripts/contract_content_compare.py` 自动执行内容比对。
  
  **2026-07-08实证**：华新镇体检合同同日发送两版（11:05和16:04），文件名完全相同，但第二版增加了费用上限17万元、项目名称加了"华新镇"、起始日期从9月10日改为9月1日——是实质不同的合同版本。因系统只按文件名判重，第二版被跳过未审查。详见 `references/queue-runner-same-name-different-content-20260708.md`。

- **auto_notify脚本依赖与失败处理**：如果邱律师的合同没有自动启动workflow，先检查`auto_notify_new_file.sh`是否还活着（`ps aux | grep inotifywait`）。该脚本没有守护机制，会静默死亡。**两种失败模式**：
  - **模式A：进程死亡** — inotifywait进程不存在。watchdog cron (`63bb31d4f050`) 会自动重启。
  - **模式B：进程活着但事件静默丢失（更隐蔽）** — 进程在跑但inotifywait没有捕获到任何事件（日志完全为空）。原因可能是：inotifywait的文件描述符失效、文件系统事件被内核丢弃、脚本启动时文件已经到达（race condition）。watchdog无法修复此模式，必须人工介入。
  - **诊断方法**：`cat ~/.hermes/logs/auto_notify.log | grep 日期` — 如果日志为空但meta文件有当天文件，就是模式B。
  
  **fallback流程**：
  1. 检查 `~/.hermes/cache/documents/` 中是否有未处理的文件（按meta时间戳筛选今天邱律师发的）
  2. 手动复制到 `Doro合同审查任务/待审查/`（用 `sudo cp` + `sudo chown www-data:www-data`）
  3. 逐个启动 workflow（`uwf thread start review-contract -p "..."`）

  **⚠️ 主动发现铁律（2026-07-03教训）**：任何操作过程中（查私信状态、查文件、核对数据等），如果发现cache/documents/中有未处理的邱律师文件（meta显示sender_id=QiuTing但对应文件不在待审查目录），必须**立即中断当前任务**，先上传+启动workflow，再继续原任务。"收到即启动"不只是auto_notify的职责——手动发现的文件也必须立即处理，不能"等会再说"。2026-07-03实证：邱律师14:27发了新合同，我在14:30+检查私信时看到了meta记录但没有立即启动workflow，直到Doro追问才处理。
  4. **多份合同时串行执行**：写 relay 脚本（等当前 thread `status=end` 后再启动下一个），因为所有合同共用 `/tmp/contract-review/` 工作目录，并行会互相覆盖
  5. **后台执行+通知**：`uwf thread exec <thread_id> --count 20 --background`，配合 `notify_on_complete=true`
  6. 可选：设 cron job 每15分钟检查 relay 进程是否存活，挂了自动重启
  
  **2026-06-29 实证**：邱律师发了12个文件，auto_notify 没运行，只有4个自动进了 workflow。手动把剩余4个从 cache 移到待审查，写 relay 脚本串行执行，成功完成。
  **2026-06-30 实证（模式B）**：邱律师发了4份合同，auto_notify进程在跑但日志完全为空（inotifywait静默失效）。手动处理全部4份。
- **Subagent/delegate_task 的清理禁令**：给 subagent 的 context 中必须明确写入"禁止删除 Nextcloud 待审查/和任务交付/目录中的任何文件。只做被要求的操作（写tracker/写xlsx/上传），不做任何清理"。2026-07-01教训：subagent 在执行 pass 操作时可能做了多余的清理动作导致文件丢失。
- **xlsx文件名列必须统一用delivered_filename**（带【修】或【无修改意见】前缀），不能写原始文件名。

## 自动清理架构（2026-06-11确认，2026-07-01补充）

活跃 cron 列表（截至 2026-07-02）：
- **`contract-cleanup`**（job_id: `4636467b715d`）：每小时，no_agent，deliver=local。清理 pass 超 24h 的文件。
- **`contract-queue-watchdog`**（job_id: `3174518affda`）：每20分钟，no_agent，deliver=local。巡检 queue-runner 是否存活，必要时重启。⚠️ 此 cron 有已知 bug：当 queue/ 中有文件未移入 done/ 时会反复重启 runner 导致重复审查（详见 `references/queue-runner-duplicate-bug-20260702.md`）。**修复前需确保 runner 加了 tracker 查重逻辑。**
- **`auto-notify-watchdog`**（job_id: `63bb31d4f050`）：每5分钟，no_agent。守护 auto_notify_new_file.sh 的 inotifywait 进程。
- 旧版`清理已交付合同`（agent模式/每6h/deliver到wecom:doro）和`合同审查调度`（每15分钟轮询）已于2026-06-11删除，与现有机制功能重复
- 新文件监控由 `auto_notify_new_file.sh`（inotifywait实时事件驱动）独立承担，不再有cron轮询

### ⚠️ 文件删除权限铁律（2026-07-01 Doro纠正）

**只有 cleanup cron 有权删除待审查/和任务交付/目录中的文件。** 手动操作（包括小Maggie和subagent）不得直接 `docker exec ... rm` 删除这两个目录的文件。

唯一例外：Doro 明确指令删除特定文件（如"删掉这个招标需求"）。

2026-07-01教训：小Maggie在执行替换/清理操作时，`docker exec rm` 误删了4份已pass但未满24小时的合同原始文件（徐泾北大居、印刷品、健康积分华新、银发健康包）。文件不在trashbin（docker exec rm绕过trashbin），cleanup cron全天silent确认没动，是手动操作误删。

### Companion 文件清理方案（2026-07-01 确认）

**Companion 类型因顾问单位而异**（不统一为"审查意见"）：
- 朱家角：审查意见文档（从模板生成）
- 爱卫中心：合同流程单（xlsx）
- 练塘：脚注（加在合同里，非独立文件，不需清理）
- 其他：无

**方案**：deliverer 上传后写 manifest → pass skill 读取 manifest 写入 tracker `companion_files` → cleanup 读取并一并删除。

**manifest 位置**：Nextcloud 任务交付目录下 `.delivery-manifest-{原文件名去扩展名}.json`（隐藏文件）。

**待落地**：cleanup 脚本需增加读取 `companion_files` 字段的逻辑；deliverer YAML 需增加写 manifest 步骤。

### 清理时机明确定义
- **触发条件**：Doro 说 pass → 写入 tracker（`xlsx_updated_at` 记录当前北京时间）
- **清理时机**：cleanup cron 每小时检查 tracker，找 `status=completed` + `xlsx_updated_at` 超过24小时 + `cleaned=false` 的记录
- **即：pass 后 24 小时清理**，不是交付后24小时、不是workflow结束后24小时
- **清理范围**：精确删除 tracker 中记录的 `original_filename`（待审查/）+ `delivered_filename`（任务交付/）+ `converted_filename`（待审查/，.doc转.docx时有值）
- **不清理的**：companion 文件（审查意见）不在 tracker 中，不会被自动清理；xlsx 在上一级目录不受影响

## 防重复审查（2026-07-01 朱家角标识牌 + 2026-07-02 夏阳/家庭医生签约）

**已pass合同被重复审查交付的根因**：

1. **relay-runner/auto_notify 不查 tracker**（2026-07-01）：启动 workflow 前不检查 tracker 是否已有 completed 记录。manifest 文件残留已 pass 合同的文件名，被重新捡起来跑了一遍。
2. **queue-runner + watchdog 交互 bug**（2026-07-02，详见 `references/queue-runner-duplicate-bug-20260702.md`）：watchdog 每20分钟重启 runner（因 done/ 不满 manifest 行数），runner 的 SKIP 逻辑只查文件是否在 queue/ 目录，**不查 tracker**。一天内 watchdog 重启 runner 41次，导致夏阳和家庭医生签约被重复审查并重复通知 Doro。

**铁律**：任何触发 workflow 的流程（auto_notify / relay-runner / queue-runner / 手动启动），**启动前必须检查 contract-tracker.json**：

**查错后否认（2026-07-01）**：Doro问朱家角标识牌怎么重复了，回答说"你没pass"——实际查 tracker 发现 seq=229 早在6/27就pass了。**被问任何合同状态时，先查 tracker/xlsx 用工具验证再回答，不凭"印象"。**

**Queue-runner + Watchdog 重复审查（2026-07-02 实证，详见 `references/queue-runner-duplicate-review-20260702.md`）**：runner等worker时异常退出→worker独立完成(含通知)→文件没移入done/→watchdog重启runner→重新审查→重复通知。**止血**：杀重复进程→清queue→移文件到done/。**Doro说收到重复通知时，按reference文件中的止血SOP执行。**

```bash
# 在 uwf thread start 之前（精确版，匹配 original_filename + status）
if python3 -c "
import json, sys
t = json.load(open('$HOME/.hermes/data/contract-tracker.json'))
completed = [c['original_filename'] for c in t['contracts'] if c['status']=='completed']
sys.exit(0 if '${FILENAME}' in completed else 1)
" 2>/dev/null; then
    echo "SKIP: already completed in tracker"
    # 移入 done/ 防止 watchdog 下次重启时再次尝试
    mv "$QUEUE_DIR/$FILENAME" "$QUEUE_DIR/done/" 2>/dev/null
    exit 0
fi
```

**手动启动时的检查**：用 `python3 -c "import json; ..."` 精确检查 original_filename + status=completed 组合。

**queue-runner 止血方法**（重复通知正在发生时）：
1. `kill` runner 进程和 background-worker 进程
2. 将已完成的文件全部移入 `done/`：确保 `done/` 数量 ≥ manifest 行数
3. 验证后 watchdog 自然停止重启（进度检查通过）

## 铁律

- **待审查目录原文件：Doro说pass之前严禁删除（2026-07-01 Doro纠正）**：无论审查了多少轮、出了多少个修订版本，原文件必须留在待审查目录，直到Doro明确说pass。违反此规则等于丢失原始文件。2026-07-01教训：重新审查反委托代发工资和生育友好合同时，把原文件从待审查删了（以为已经审查完），Doro发现后要求恢复。恢复方法：从cache/documents/复制原文件回到待审查目录。
- **Nextcloud 待审查/和任务交付/目录文件：只有 cleanup cron 有权删除（2026-07-01确立）**：手动操作（包括小Maggie本人、subagent/delegate_task）不得使用 docker exec rm 删除这两个目录的文件。唯一例外：Doro 明确指令删除特定文件（如"删掉这个招标需求"）。2026-07-01教训：今天pass的4份合同（徐泾北大居、印刷品、健康积分、银发健康包）原始文件和交付文件在pass后不到24小时即被删除，tracker显示cleaned=False，cleanup cron全天silent——是手动操作误删。根因无法追溯。
- **手动操作时 workflow 规则同样适用（2026-07-01确立）**：手动执行合同审查相关操作（修订、交付、清理）时，必须遵守 workflow YAML 中各角色的职责边界和规则约束。不能因为"我知道怎么做"就跳过规则。
- **不主动生成审查意见文档（2026-07-01 Doro纠正）**：除非Doro明确要求，否则pass流程只处理【修】修订版，不主动生成【审】审查意见文档。审查意见是额外交付物。
- **方案不等于授权执行**
- 新方案提出后，Doro会问"会有什么影响吗"——必须主动分析潜在影响（token消耗、误判风险、时区问题、单点故障、竞态条件等），不能只说好处。被要求"再想一想不要有漏洞"时，逐一列出漏洞清单+解法，不能遗漏。复杂度过高时Doro会直接砍掉——接受并简化。
