Files
dsh_shenxian/dsh-server-docs/04-调整方案/73-让锁真正拦得住人-措辞修正与PreToolUse强制钩子.md
T
admin 5ad755116e chore(docs): 文档库并入代码仓(R4 选 a)+ 索引/台账跟进
1) dsh-server-docs/ 从工作区(原 E:\...\aliyun-dsh-server\dsh-server-docs)**整体并入本仓**,
   保留目录名 ⇒ 仓库内 dsh-server-docs/... 的相对引用天然继续有效;旧目录(含其 .git)已归档到
   工作区 _中间产物_待清理/,未随本提交带入。
2) .gitattributes:新增 `dsh-server-docs/** -text` —— 原文档库是 `* -text` + autocrlf=false,
   必须保持纯 LF,否则会被本仓的 CRLF 规则翻掉。
3) 活引用里的绝对路径已全部改到新位置(docs 的 INDEX / README / scripts / skills + 用户级 skills
   + ~/.workbuddy/settings.json 的 hooks);历史档案(04-调整方案/、archive/)按「只增不改」未动。
   ⚠️ hooks 路径改动需「完全重启会话」才生效(配置是会话启动快照)。
4) 交接单/T08:新增 §16「生产整体切换执行记录」(形态 / 落地动作 / **4 个只有真上线才暴露的真 bug** /
   验收证据 / 回滚命令 / 残留项);台账 T08 行 → 已完成并归档;03-路线图 §二 登记 T08 收尾项。
5) 统一称谓:**「本机」只指跑 WorkBuddy 的开发机**,47 / 106 一律写「远程服务器」。
2026-09-15 18:47:13 +08:00

20 KiB
Raw Blame History

73 · 让「锁」真正拦得住人:措辞修正 + PreToolUse 强制钩子

  • 日期:2026-09-12
  • 触发:用户「会话并行修改不是应该有锁的机制吗,知道这个机制吗」→ 复盘发现机制存在,但当天被跳过两次(含我自己的那一次)
  • 结论一句话:机制不缺,缺的是强制。根因有两条 —— ① 没有任何强制入口(settings.json 的 hooks 段实测为 null)② guard 的输出语义有歧义:「无全局锁」被读成「环境干净,可以开工」,而正确读法是「你快去抢锁」。本次两条都治:措辞修正(库内)+ PreToolUse 强制钩子(配置层,无锁直接拒写)。
  • 状态:✅ 已实现并单点验证;⚠️ 钩子待用户在 /hooks 面板审核启用(外部改 settings.json 需审核才生效)
  • 关联:档案 69(三把锁的建立)/交接单/README.md §一 · §三 0·13 · §六 /scripts/handoff-guard.sh · op-lock.sh /scripts/lock-guard-hook.py(本次新增)/决策方法 dsh-decision-method §4.4

一、问题:机制有,人没遵守

谁 情况
本会话 做 T06(实例回收自动恢复)时,只跑了 handoff-guard.sh 的信息模式 —— 看到「(无单级锁)(无全局锁)」→ 读成"环境干净,可以动手",直接开工。正确动作是:没锁 ⇒ 下一步就是抢锁。
另一个会话 改 README.md / DEPLOY-本部署.md / skills/…/SKILL.md 时也没占锁(当时三把锁全空)。

定性(按 dsh-decision-method X3 的判据):冲突只认两个硬信号 —— 别人的占用锁 + 待推送清单里的未声明文件。按此复核,双方改的文件不重叠,没有产生实际损害 → 本次属流程失效,不是事故(避免过度反应)。但它确实在同一文件 INDEX.md 上留了残留(对方 1 行改动被我的 commit 收进去)。

二、根因(两条)

  1. 没有强制入口 —— 三个位置的 settings.json(用户级 / 项目级 / 库内)hooks 段全为 null。锁完全靠"自觉"。
  2. handoff-guard.sh 的输出语义有歧义 —— 「✓ 无全局锁」在视觉上像"检查通过",而它的真实含义是"尚未持锁"。这是本会话踩坑的直接原因,是我自己写的文案没说清。

三、方案对比(按 dsh-decision-method §4.4 裁决顺序)

方案 内容 判定 理由
a. 措辞修正 guard 无锁时明确输出「⚠ 不是可以开工,是你快去抢」+ 交接单补语义 ✅ 采用 治第 2 条根因;库内改动、零风险
b. 只做 a —— ❌ 约定拦不住不看的人(今天已证)
c. PreToolUse 强制钩子 写本库/代码库时无锁直接 deny + SessionStart 打印锁状态 ✅ 采用 治第 1 条根因;§4.4 第 3 条「能配置解决就不改码」命中(hooks 是配置层,免 build 免重启)
d. 拦 Bash 连 bash 写文件也拦 ❌ 不做 会把自己锁死(抢锁命令本身要能跑);Bash 写文件是少数派且破坏面可见(git status)
e. 靠自觉 —— ❌ 今天已证明不可靠

十问自检(关键四条):影响谁 → 仅本库/本代码库(路径前缀匹配,对其他项目零影响)|断多久 → 不中断任何服务|回滚 → 删 settings.json 的 hooks 段或面板关闭|验收 → 四态命令可复现(见 §四)。

四、实现

4.1 措辞修正(库内)

文件 改动
scripts/handoff-guard.sh ① 【1】单级锁「✓ 无人占用」→ 追加「⚠ 这不是「可以开工」,是「你快去抢」」+ 抢锁命令
② 【1c】「✓ 无全局锁」→ 改为「⚠️ 这不是「可以开工」,是「你快去抢」」+ 命令 + 一句判据(「环境干净」≠「没人动过」)
③ 信息模式结论行追加同样的提示,并注明 2026-09-12 的实证
交接单/README.md §一 两级锁小节补一条:「「无锁」的正确读法 =「你快去抢」;看到无锁 ⇒ 下一个动作就是 --claim-exec;抢不到 = 有人在跑 = 停手」

4.2 强制钩子(scripts/lock-guard-hook.py,新增)

事件 行为
PreToolUse(matcher Write|Edit) 目标路径落在受保护根内 且 .exec-lock 不存在 → 输出 permissionDecision: "deny",理由含可直接粘贴的抢锁命令
SessionStart 打印锁状态:已占用 → 显示占用者;空闲 → 提醒「空闲 ≠ 可以开工,动手前先抢锁」

作用域(刻意收窄):只判两类根 —— DSH_DOCS_ROOT(默认 E:\ProgramData\AI技能\aliyun-dsh-server\dsh-server-docs)与 DSH_CODE_REPO(默认 D:\github\dsh_shenxian),可用 env 覆盖。其余路径一律放行 → 对其他项目零影响。

四态单点验证(用临时库模拟,真锁未被触碰):

# 场景 期望 实测
① 受保护路径 + 无锁 deny ✅ 返回 deny + 完整理由 + 抢锁命令
② 受保护路径 + 有锁 放行 ✅ 空输出
③ 非受保护路径 放行 ✅ 空输出
④ SessionStart(有锁态) 提示占用者 ✅ 「🔐 全局执行锁【已被占用】:tester」

异常安全:payload 解析失败 / 脚本内部异常 → 一律放行(except: pass + 退出码恒 0),绝不因为钩子自身问题阻断工作。

4.3 启用方式(需用户操作)

写入 ~/.workbuddy/settings.json 的 hooks 段:

{
  "hooks": {
    "PreToolUse":  [ { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "python \"E:/ProgramData/AI技能/aliyun-dsh-server/dsh-server-docs/scripts/lock-guard-hook.py\"", "timeout": 10 } ] } ],
    "SessionStart":[ { "matcher": "startup",    "hooks": [ { "type": "command", "command": "python \"E:/ProgramData/AI技能/aliyun-dsh-server/dsh-server-docs/scripts/lock-guard-hook.py\"", "timeout": 10 } ] } ]
  }
}

⚠️ 外部修改 settings.json 需在 /hooks 面板审核后才生效(本次由我写入配置,启用动作留给用户)。

五、为什么刻意不拦 Bash(重要)

  1. 抢锁命令本身必须能跑 —— 否则「拿不到锁就不能改文件、不能改文件就抢不了锁」= 死锁;
  2. Bash 写文件是少数派,且破坏面可见(git status / 对账能查出来);
  3. 宁可留一个显式的安全阀(Bash 可绕),也不要一个可能导致死锁的强制层。

六、回滚

  • 措辞:git revert <本次 commit>。
  • 钩子:删 settings.json 的 hooks 段(或在 /hooks 面板关闭);lock-guard-hook.py 为独立文件,可留可删。
  • 钩子不加任何持久状态,不写锁、不写日志。

七、遗留 / 边界

  1. 归属判定未做:钩子只判「有没有锁」,不判「锁是不是你的」(hook 拿不到会话名与锁 OWNER 的对应关系)。这已足够 —— 今天的失败模式正是"两边都没抢锁",钩子会两边都拦,而抢锁是原子的 → 串行化达成。
  2. Bash 是显式缺口(见 §五),不修。
  3. 钩子只在 WorkBuddy 层生效:若换个工具(或用户手工编辑文件)绕过,仍以约定与台账为准。
  4. 本次未改动 04-调整方案/ 既有档案(遵守库内新约定「档案只增不改」);与此前档案冲突处一律以本档 + 入口文件为准。

八、补:让规则真正到达每个会话(用户追问后)

用户追问:「关键是别的会话怎么知道遵循这套规则,比如另外 5 个会话也在执行 dsh 服务相关任务」

核实结果 —— 这是真缺口:

载体 何时被会话看到 现状
~/.codebuddy/CODEBUDDY.md(用户级) 启动时注入 ❌ 不存在(~/.codebuddy/rules/ 也没有)
项目根 CODEBUDDY.md 启动时注入 → 覆盖本工作区所有会话 ✅ 存在,且已有 §6「并发纪律」—— 但内容有漏(见下)
.codebuddy/rules/*.md 条件注入(碰对应文件时) ✅ 3 个(archive-doc / frontend-ui / server-ops)—— 均未提锁
dsh-server-docs/CODEBUDDY.md 条件注入(碰库内文件时) ✅ 存在 —— 管文档库约定,未提锁
Skills(dsh-change-workflow / dsh-feature-first) 被触发才加载(不保证) 有「三把锁」章节,但不保证到达
SessionStart / PreToolUse hook 会话启动 / 动手时 已配,待审核

缺口精确到两处 —— 都在唯一可靠的载体里:

  1. §2 表格:「改任何文件之前 → 跑 handoff-guard.sh」—— 只写「跑」(检查),没写「抢」。 这正是我当天翻车的同一处("跑了检查、看到无锁、直接动手")。
  2. §6 并发纪律:只有「--claim <单号>」(细锁,管"同一个单别被两人做"), 漏了「全局执行锁 --claim-exec」 —— 而它才是"多个会话同时干活"场景的唯一防线 (细锁管不住跨单撞车:5 个会话各做各的单,细锁互不冲突,但都会改 README/INDEX/03-路线图)。

已补齐(改 CODEBUDDY.md,改前已手工备份):

位置 改为
§2 该行 「改任何文件之前(第一步,不是"检查"是"抢")」+ 完整 --claim-exec 命令 + 「抢不到 = 停手」
§6 新增「三把锁,顺序固定」表(粗/细/生产,含各自命令与职责)+ 「「无锁」= 你快去抢」读法 + 指向本档与 lock-guard-hook.py

⚠️ 两条必须知道的限制

  1. CODEBUDDY.md 是启动时加载 → 本次改动对已经在跑的会话无效(要重启才重载)。 ⇒ 对"已经在跑的 5 个会话",唯一即时生效的手段是:
    • hook(PreToolUse 会硬拦,无需会话配合);
    • 或用户直接在那些会话里说一句「动 dsh 前先抢锁」(最直接,一轮见效)。
  2. 项目根 CODEBUDDY.md 不在任何 git 仓库内(项目根不是仓库,也不在文档库仓库里)→ 无版本保护,只能靠手工备份(本次已留 .bak-locksect-<ts>)。 建议把规则文件纳入版本管理(否则它的改动既无锁保护、也无历史)—— 属"是否需要新造机制", 按 §4.4 留作独立决策,本次未做。

九、启用步骤(用户在 WorkBuddy 里操作)

/hooks 面板在哪

在 WorkBuddy 的对话输入框里输入 /hooks(斜杠命令)→ 打开 hooks 配置面板。 官方文档原文:"/hooks CLI panel for reviewing and approving any configuration changes before they take effect, ensuring safety." (外部改 settings.json 必须经此面板审核/批准才会生效 —— 这是官方安全机制,不是我这边的限制。)

步骤

  1. 输入框敲 /hooks 回车 → 面板打开
  2. 找到 PreToolUse(matcher Write|Edit)与 SessionStart(matcher startup)
  3. 审核 command 指向 dsh-server-docs/scripts/lock-guard-hook.py → 批准/启用
  4. 按 Esc 返回

启用前的加固(2026-09-12 实测)

官方文档明确:Windows 上 hooks 强制走 Git Bash(cmd.exe / PowerShell 不支持)→ 命令里的裸 python 若不在 hook 的 PATH 里就会直接失败。故把 command 改为绝对路径:

"D:/miniconda3/python.exe" "E:/ProgramData/AI技能/aliyun-dsh-server/dsh-server-docs/scripts/lock-guard-hook.py"

实测记录:env -i "D:/miniconda3/python.exe" "<中文路径>/lock-guard-hook.py"(干净环境、不继承 PATH) → 退出码 0、行为正确(持锁态放行 / 无锁态 deny)。即:绝对路径 + 中文路径均可用。

启用后自检(第三方可复现)

# 操作 期望
1 无锁状态下,让任一会话改 dsh-server-docs/INDEX.md 被拦,并返回可直接粘贴的抢锁命令
2 先 --claim-exec 抢锁,再改同一文件 通过
3 改 D:/其他项目/xx.md(不在受保护根内) 通过(其他项目零影响)
4 新开会话 启动时打印锁状态提示

若不启用

库内约定仍然存在(项目根 CODEBUDDY.md §2/§6 + dsh-server-docs/CODEBUDDY.md + 交接单/README.md §一), 但只对"读过它"的会话有效 —— 这正是"另外 5 个会话不知道"的成因(见 §八)。 ⇒ 对已在运行的会话,hook 是唯一不需要它们配合的到达方式。


十、⚠️ 修正:桌面版没有 /hooks 面板(2026-09-12 17:45,用户实测反馈)

用户反馈:按 §九 输入 /hooks —— 什么也没有。

修正:/hooks 是 CodeBuddy CLI 的命令,WorkBuddy 桌面版没有这个面板。 §九 的操作步骤对桌面版不适用。 成因如实记:那是照着 CLI 文档写的,没在桌面版核实 —— 教训:跨端能力先在目标端实测,再写进文档(同 §4.3 验收口径分级:L1 推断不能当 L5)。

桌面版的正确加载方式(第三方探针实测记录)

① ✅ 2026-09-13 实测确认本行原文正确:hook 命令在会话启动时快照,改完 settings.json 对已在跑的会话无效(须「完全重启」或新开会话)。⚠️ 当天一度被误写成「每次调用现读」、并据此宣布本行作废,已纠正(成因见文末「修正」节)。路径失配时 fail-closed —— hook 打不开脚本 → 报错 → 该机所有会话的 Write/Edit 全被拒。别把「被拒」当成"锁被别人占了",先去核对 hook 里的绝对路径。 ② 关窗 ≠ 退出 —— WorkBuddy 有常驻能力,点关闭按钮只是关窗口、进程还活着,必须彻底退出。

本机实测印证:当前有 5 个 WorkBuddy.exe 进程在跑(见 17:42 的 tasklist 输出)→ "关窗不退出"属实。

正确步骤(Windows)

  1. 彻底退出 WorkBuddy:托盘图标右键 → 退出;或任务管理器结束所有 WorkBuddy.exe(含后台进程)

  2. 重新启动 WorkBuddy

  3. 验证是否生效 —— 本次为此新增了自证手段:hook 会写低频日志到 E:\ProgramData\AI技能\aliyun-dsh-server\.workbuddy\lock-hook.log(可用 DSH_LOCK_HOOK_LOG 覆盖)

    2026-09-12 17:42:17   SessionStart       空闲 source=startup
    2026-09-12 17:42:18   PreToolUse-deny    Edit D:/.../a.md
    
    观察 含义
    重启后出现 SessionStart 行 ✅ 配置已加载、hook 已生效
    之后无锁改本库被拦 → 多一行 PreToolUse-deny ✅ 强制层工作正常
    两行都没有 ❌ 未生效(回来反馈,改走下一步)

    日志只记 SessionStart 与 deny 两类(低频),不记录每次写操作,避免刷屏。

若桌面版最终不支持 hooks

则本机制退化为「约定 + guard」,届时需要另找"到达方式"。按 dsh-decision-method §4.4, 届时候选顺序为:① 让 --claim-exec 成为每次动手前的固定动作(写进 CODEBUDDY.md,已做) ② 把检查做进更早的钩子点(若桌面版有其它可挂的事件)③ 由用户在每个会话里说一句(最直接但需人工)。


十一、⚠️ 修正:禁止「人工删锁 / 接管」(R9,2026-09-12 用户明令)

用户原话:「严格禁止这类操作必须!!!!!!记录到红线中」,并引用本库当时仍在流传的一句操作指引:

接管 —— 按交接单的流程人工删锁(rm -rf 交接单/.exec-lock)由我接手。但这等于判定那个会话已死/已完成,风险是它其实还在跑 → 撞车,所以我需要你明确点头。

定性:这句话本身就暴露了机制缺陷 —— 它把"接管"的判据交给人的感觉("疑似已死"),而平台没有心跳机制,AI 没有任何客观判据能确认对方是否还在跑。删锁 = 在无法验证的前提下单方面撤销互斥 → 一旦对方仍在跑,就退回「两个会话同时改同一批文件」,而后者正是三把锁存在的唯一理由。

根因不是人不懂,是文档在教:当时全库有 4 处写着"人工删锁",连 handoff-guard.sh 自己的抢锁失败提示里都在教。

处置(2026-09-12 18:50 本库,持全局锁下完成):

# 位置 改动
1 项目根 CODEBUDDY.md §3 新增红线 R9(另一并行会话先行落地,已核对内容一致)
2 项目根 CODEBUDDY.md §6 增「⛔ 抢不到锁就是终点,不是待办」一条
3 dsh-server-docs/CODEBUDDY.md 锁小节增 R9 指针 + 「guard 输出不构成授权」
4 交接单/README.md §三 13 条 「接管:…人工删锁…」→ 改为禁止,原文标注「已作废」
5 交接单/README.md §三 11 条 「接管必须无损」加限定:接管动作不得由 AI 自行发起
6 scripts/handoff-guard.sh(2 处:抢锁失败提示 + 信息模式提示) 删掉"人工删锁 / 接管",改为 R9 禁止 + 停手 + 报告用户
7 skills/dsh-change-workflow/SKILL.md(工作副本 + 归档副本,两处 md5 需一致) 三把锁章节增 R9 条目

R9 唯一合规路径(判据):

  1. 抢不到锁 ⇒ 停手(不重试、不"抢一下看看"、不绕道);
  2. 报告用户 —— 说清「谁占着 / 占多久 / 我卡在哪」,而不是自己找理由;
  3. 锁的处置权只属于用户本人 —— 要撤也只能用户自己动手,AI 不得代判断;
  4. 仅在「用户已点头 + 用户自己撤锁之后」,才读 OWNER + 台账进度续做(T03 的 A→B 那套只适用于这种情形)。

验证:bash -n scripts/handoff-guard.sh → 语法 OK;全库 grep -rn 人工删锁 → 只剩"禁止性条款 / 已作废说明",无任何还在教人删锁的位置。

对本档上文的影响:§七 第 1 条(钩子只判"有没有锁"、不判"锁是不是你的")在 R9 下更关键 —— 无锁时 hook 两边都拦(天然串行化),但有锁时它不判归属,所以"这是别人的锁"仍只能靠 guard + R9 拦人, 这正是本次把措辞从"可以删锁接管"改成"停手报告"的原因。


修正(2026-09-13):hook 路径随工作区迁移

工作区 2026-09-13 由 D:\AI技能\aliyun-dsh-server 迁至 E:\ProgramData\AI技能\aliyun-dsh-server;本档上文的 hook 命令片段与 DSH_DOCS_ROOT 默认值已同步改为新路径(实体在 ~/.workbuddy/settings.json 与 dsh-server-docs/scripts/lock-guard-hook.py)。代码仓 D:\github\dsh_shenxian 未变动。

迁移时踩到的坑(值得记住):工作区一搬,settings.json 里 hooks 的绝对路径立刻失配 → python 打不开脚本 → hook 报错 → 该机所有会话的 Write/Edit 全被拒(2026-09-13 实际发生,连"改 settings.json 本身"都被拦)。 语义定论(2026-09-13,含一次自我纠错):hook 命令是「会话启动时快照」 —— 本会话 06:47 启动 → 06:55 把 settings.json 的 hook 路径改成 E: → 07:05 拆掉临时目录联接后,Write/Edit 报的仍是旧路径 D:/AI技能/... ⇒ 改配置对已在跑的会话无效。 ⚠️ 期间一度写成「每次调用现读、无需重启」并据此宣布「目录联接多余」—— 实为联接在 06:53–07:05 存在,把「改完就能写」伪装成了现读(同机另一会话也据此得出同样错论,可见这就是该误判的成因)。 正确处置:改完 hook 路径 → 完全重启 WorkBuddy(关窗 ≠ 退出)或新开会话;重启前该机所有会话的写操作一直被拒。应急兜底:本钩子不拦 Bash(有意留的安全阀)⇒ 可用 shell 写文件过渡(09-13 实际走通)。 (临时目录联接已按用户要求移除;D:/AI技能 现无任何实体目录或联接。)