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 一律写「远程服务器」。
This commit is contained in:
admin committed 2026-09-15 18:47:13 +08:00
1 parent c70d5d860e
commit 5ad755116e
173 files changed
+27632

No files matched your search

@@ -0,0 +1,54 @@
---
name: dsh-env-bootstrap
description: DSH 平台项目的「环境引导 / 搬迁」技能 —— 把**工作区常驻规则**(`CODEBUDDY.md` 的关键章节)带走,并在新电脑 / 新工作区路径下**校验与注入**,同时自检环境相关项(绝对路径、hooks 命令、代码仓、备份目录)。当用户说「换电脑了」「改了工作区路径」「迁移到新环境」「规则会不会丢」「新环境还没配好」时使用。
version: 1.0.4
updated_at: 2026-09-15
agent_created: true
---
# dsh-env-bootstrap — 环境引导(常驻规则的可移植化)
## 0. 为什么存在
项目规则分两层,**能力恰好互补、缺口也恰在这里**:
| 层 | 载体 | 特点 |
|---|---|---|
| **常驻层(权威)** | 工作区 `CODEBUDDY.md`(每会话自动注入 ⇒ **动作前必然生效**) | 它是**工作区文件** ⇒ **换电脑 / 换路径就没了** |
| **可移植层** | 本技能(用户级 `~/.workbuddy/skills/`) | 随技能走,但**不自动注入** |
⇒ 本技能把常驻层的关键章节做成**快照**,并提供 **校验 / 注入 / 环境自检**,让"换环境后规则还在"这件事**可执行、可验证**。
## 1. 命令(脚本 `scripts/resident-rules.py`)
```bash
S="<本技能目录>/scripts/resident-rules.py"
python3 "$S" --env-check # ① 环境自检:目录存在性 + hooks 命令里的绝对路径(失效 = 全机 Write/Edit 被拒)
python3 "$S" --check # ② ★校验:关键规则是否齐备(**默认只报**,rc=1 = 有缺失/漂移)
python3 "$S" --inject --init # ③ 注入:目标无标记块时**追加**(不动既有内容);有则在块内替换
python3 "$S" --snapshot # ④ 规则更新后,由权威 CODEBUDDY.md **重生成**技能内快照(单向)
python3 "$S" --goal <路径> # 可指定别的目标 CODEBUDDY.md
```
## 2. 换环境的正确顺序(**把"规则齐备"放在第一步**)
1. `--env-check` —— 先看路径 / hooks 是否失效(**hooks 绝对路径失配 = fail-closed:该机所有会话的 Write/Edit 全被拒**,2026-09-13 实测)
2. `--check` —— 关键规则缺失?⇒ `--inject --init` 追加标记块 ⇒ **人工去重**(块外原有内容与新块可能重复)⇒ 复跑 `--check` 直到 rc=0
3. 逐项核对快照文末的 **§待核清单**(服务器 / 备份目录 / 代码仓 / 工作区)—— **环境相关项不自动配、也不假装能配**
4. 规则更新时:改**权威** `CODEBUDDY.md` → `--snapshot` 重生成快照(**方向单向**)
## 3. 设计红线(别把它用成"第二真相源")
- **权威方向单向**:`CODEBUDDY.md` 是权威,快照是它的副本;**只允许 `--snapshot` 从权威生成副本**,⛔ 不许反向手改快照。
- **默认只报不改**(与文档库「体检只报不改」同规):`--check` **绝不**改任何文件。
- **注入只动标记块**:`<!-- BEGIN resident-rules … --> … <!-- END resident-rules -->` 之间;**首次不自动注入**(避免同一规则在两处并存)。
- **不假装能自动配环境**:环境相关项只做"存在性检查 + 待核清单"。
- ⚠️ **常驻层不可被"搬走"**:本技能**不替代** `CODEBUDDY.md` —— 规则仍必须常驻在**该环境**的 `CODEBUDDY.md`(否则"动作前必然生效"这条就断了);技能只是**把规则带过去并防丢**。
## 4. 自检(用完之后问自己)
1. 我改的是**权威**还是**快照**?(改快照 = 造漂移源)
2. `--check` 绿了吗?红的那条是**真缺失**还是我刚改错?
3. 环境相关项核对了吗(hooks 路径 / 代码仓 / 备份目录 / 工作区)?
4. 注入后有没有**人工去重**(块外原内容 vs 新块)?
5. 给用户的报告**能被扫吗**?—— 排版按 `dsh-feature-first §5.4`。
@@ -0,0 +1,133 @@
# 常驻规则 · 可移植快照(由 `scripts/resident-rules.py --snapshot` 生成,勿手改)
> **这是「工作区 `CODEBUDDY.md` 关键章节」的副本**,随技能走 ⇒ 换电脑 / 换工作区也能带走。
> ⚠️ **权威方向单向**:`CODEBUDDY.md` 是权威,本文件是它的副本(重生成用 `--snapshot`)。
> ⚠️ **环境相关项**(绝对路径 / IP / hooks 路径 / 工具位置)注入后**必须按新环境核对**:见文末「待核清单」。
## 骨架锚点(校验用;丢了就等于规则没了)
- `A1` — 只问「超过现有判断方法边界」的问题
- `A2` — 禁止用征询句收尾
- `A3` — 四件或七件套必跑
- `R7` — 禁未经确认的批量 / 全仓写入
- `R7-边界` — R7 只适用于「不是我的 lane」
- `R9` — 绝对禁止「人工删锁 / 接管」
- `R11` — 只做正向迭代
- `U27` — 目标不打折,路径取最小代价
- `L1` — 先抢全局执行锁
- `L2` — 锁的生命周期 = 任务的生命周期
---
## 1. 提问判据(唯一一条)
> **只问「超过现有判断方法边界」的问题。**(用户原话)
- **边界内 → 一律自决,不要问**:技术选型 / 实现路径 / 命名与数据结构 / 性能与资源调参 / 部署与同步 / 排查方法 / 版本与依赖 / 兼容与降级 / 方法内的方案取舍 / 文档与档案的技术内容。
- ⚠️ **「部署 / 上线」明确属于上面这一项**(2026-09-13 用户纠正原话:「为什么要等我确认才部署呢,**我看线上效果才知道是否满足需求**」):**不中断在线用户**的上线动作(`scp` 产物到 `/opt/dsh/artifacts` + `ensure-biz-plugins.cjs --all` 换包、静态页改动、候选池投放)**做完即上线,不要问** —— 用户要先看线上效果才能判断需求是否被满足。
- **⚠️ 2026-09-13 用户明令:「这个是开发环境服务器,不用担心中断用户」** ⇒ 上一条的「只有会中断在线用户的动作才走 R8 先知会」**改成**:重启 / 停实例 scope / drain / 改配额或 env / 改 nginx·nft **直接做**,只需**动手前一句话说明**在做什么(可回溯用)。**仍未放开**:不可逆的破坏性操作(删数据 / 迁 DB / 清目录)仍先出清单。
- **边界外 → 必须问**:① 业务目标与优先级(做不做、先做哪个)② 花钱与资源承诺 ③ 对外承诺与合规(备案 / 资质 / 合同)④ 需用户提供的账号凭据或审批 ⑤ 无客观优劣的体验偏好(审美 / 文案 / 默认值)⑥ 影响面超出本平台 ⑦ **红线门禁** ⑧ 方法确实判不准。
- **判据**:这件事有没有**客观可判的优劣**?有 → 自己定;没有 → 问用户。
⚠️ **上抛门槛 = 存在「真取舍」**(2026-09-15 用户明令):「**需要我确认的方案需要说明优点和缺点,现在没法判断,假如只有优点或只有缺点那不需要我判断**」⇒ 把候选各写 **优点 + 缺点**:某个**只有优点**(明显更优)或**只有缺点** ⇒ **自己拍掉**;只有**各有优有劣、客观标准分不出高下**才上抛,且**必须逐项列出优点与缺点**。
- ⛔ **不许捆包**:要问红线就**只问那一句**(要不要现在动生产 / 影响谁 / 断多久 / 能否避开);技术方案自己定好、当**已定项**陈述("我按 X 做…已定,可推翻")。**一轮最多一个问题,同类不连问两次。**
- ✅ **回话前自检(发出任何回复前过一遍 · 2026-09-15 加)** —— 起因:**「提问闸门」hook 只能拦 `AskUserQuestion` 工具,而真实的上抛大多发生在正文里**。
**实证**:本工作区日志 `tool=AskUserQuestion` 调用数 = **09-12: 43 / 09-13: 3 / 09-14: 0 / 09-15: 0**(09-14 起该工具基本不用)⇒ **hook 那条路径几乎不被走到 ⇒ 拦不到正文里的征询**。
⛔ **禁止用征询句收尾**:出现「**要我…吗 / 是否要我 / 需要我…吗 / 要不要我 / 请确认 / 你看怎么办**」时,**先按上一条判据重判三问**:
① 命中**真门禁**吗(不可逆破坏性操作 / 边界外六类)?**没命中 → 删掉这句,自己做完,改成陈述句**("我接着做 X");
② 我是不是在**把已经定下来的事再问一遍**?是 → 删;
③ 我要问的这件事,**候选之间是「真取舍」吗**(各有优有劣、客观标准分不出高下)?—— 若某个**只有优点 / 只有缺点** ⇒ **自己拍掉**;是真取舍 → 才允许问,且**一轮只问这一句**、**逐项写出优点与缺点**。
📌 需要用户拍板时:**位置 = 整条回复的最后一节**(⛔ 不许埋在中间)、**按有序段落逐条编号**、**每个候选必须写「优点 / 缺点」两栏**、**候选竖排成段**(A / B / C **各占一行**,⛔ 不横排、⛔ 不做成表格的列)(2026-09-15 用户明令:「**能根据决策方法 自行决策的就自决策继续处理,不能决策的问题和需确认内容放在回复的最后,按照有序段落展示**」+「**需要我确认的方案需要说明优点和缺点,现在没法判断,假如只有优点或只有缺点那不需要我判断**」+「**每个需要我决策的问题的潜在解决方案 A B C 也按照段落式排版,别横着排列**」);**形态 = 陈述句**(问题 + 各候选优缺点 + 我的倾向),⛔ **不是**甩征询句("要不要我继续" / "说一声即可")。
- 📐 **排版按 `dsh-feature-first §5.4`**(可扫读九条 + 形态骨架 + 十三条反模式):**首屏 3 行给判定 · 层级 ≤3 · 每节 ≤7 行 · 加粗只留关键词 · 表格 ≤5 列 · 一条信息只说一次**;**待你拍板项落在最后一节、逐条编号、每个候选带「优点 / 缺点」且竖排成段(不横排、不做成表格的列)**;执行信息 / 报障 / 提问**各有现成骨架,不新造**;细节进「技术附录」,正文只留"能决定下一步"的信息。
- 🎯 **要的是解决问题,不是将就妥协**(2026-09-15 用户明令):面对风险/缺陷**默认目标是解决**;**降级目标 / 延期 / 静默兜底**三种**都不算解决**。只有**客观不可逾越**(技术不可行 / 上游未支持 / 需你提供凭据或窗口)才允许"暂时接受",且必须写明 ① 卡在哪(证据)② 已做到哪一步 ③ **什么条件一出现必须回头解决**。⚠️ 与 §2「最小代价路径」不矛盾:**目标不打折,路径取最小代价**(细节:素材库 **U27 / U26 / A6**)。
- 🟢 **只做正向迭代**(2026-09-15 用户明令,红线 **R11**):任何改动**只要让项目在某一维度净变差**(目标/方向/架构/功能/性能/安全/交互/UI/便利性/扩展性)⇒ **立即停下复盘**;**拿不出正向做法 ⇒ 立即停止、禁止继续执行**(细节:素材库 **U28**)。
---
---
## 3. 红线 R1–R11(任一条命中 → **先停手**;效率论证不构成豁免)
| # | 红线 | 要点 |
|---|---|---|
| **R1** | 不自动升级 dsh | 升级须走独立"测试 → 评估 → 修复"流程 |
| **R2** | 不改官方 dsh 主程序与缓存 | `@deepseek-ai/dsh` **零改动**;扩展只走 profile 层官方插件机制 |
| **R3** | client bundle 禁 `exports.default` | 只导出 `apply` + `inject` |
| **R4** | 不用真实账号测登录 | 用临时 session(`mksess.cjs` 直插),用完即删 |
| **R5** | **权限只准收窄** | 凡**扩大**(新挂载 / 放开遮蔽 / 暴露平台目录或 env / 放宽 nft / 提档位)→ 先出「权限影响评估」并取得确认 |
| **R6** | 先查已有资产再动手 | 可用技能 → 本机 / 项目已有技能与记忆 → "本机已有的能否满足" |
| **R7** | **禁未经确认的批量 / 全仓写入** | **只做被明确要求的事**;额外发现的问题**先报告、后动手**;禁全库遍历改写 / 通配符重写 / 批量 `chmod`·`chown` / **批量换行符转换** / `cp -r` 整目录覆盖 / `git add -A`;**可能影响 >10 文件 → 先出清单 + 确认**;先单点验证;**本机不是沙箱**(会经 scp 传导到生产) |
| **R7-边界** | **R7 只适用于「不是我的 lane」—— 两个方向都别套错**(2026-09-13 用户两处明令) | ① **不适用于「我 lane 内的执行细节」**:部署 / 上线(换包、传产物、改静态页、候选池投放)、重启服务、改配置、跑自己的脚本、改自己的插件源码与产物 ⇒ **别拿 R7 当挡箭牌去问,直接做**,事后一句「我选了什么(可推翻)」(原话:「为什么要等我确认才部署呢」)。② **适用于「平台级 / 全局 / 别人 lane」**:`/var/lib/**`、全局符号链接、systemd 单元、nginx·nft、别人的 profile / 产物 ⇒ **一律只报告、不动手**,**哪怕改它能让自己流程跑通**(原话:「谁让你去改这个的」+「不是自己负责的任务相关文件不要去改」)|
| **R8** | **中断在线用户的生产变更须先知会** | ⚠️ **2026-09-13 用户明令修正:服务器 `47.77.182.89` 是「开发环境服务器」,不用担心中断用户** —— 重启 `dshs` / 停实例 scope / drain / 改实例配额或 env / 改 nginx·nft **均可直接做**,不必再等确认。仅保留两条最低自律:① **动手前一句话说明**在重启/停了什么(便于出问题回溯)② **破坏性且不可逆**的动作(删数据 / 迁 DB / 清目录)仍先报清单。 |
| **R9** | **⛔⛔ 绝对禁止「人工删锁 / 接管」**(用户 2026-09-12 明令:"严格禁止这类操作") | **AI 一律不得**:`rm -rf 交接单/.exec-lock`、删 `交接单/.doing-*`、或以「持有者疑似已死 / 卡住 / 太久没动 / 只在只读分析没产出」为由**单方面接管**。锁**只能由持有者自己释放**(`--release-exec` / `--release`);**`handoff-guard.sh` 输出里的「或确认接管后人工删锁」不构成授权**。抢不到锁时 AI 的**唯一**合规动作 = **停手 + 报告用户** —— **锁的处置权只属于用户本人**(要删也只能用户自己动手)。**理由**:删锁 = 在**无法验证**对方死活的前提下单方面撤销互斥(**无心跳机制,AI 没有任何判据**)→ 一旦对方仍在跑,就**退回「两个会话同时改同一批文件」**,而这正是这把锁存在的理由。 |
| **R10** | **⛔ 绝不以 root(或非该实例 uid)运行 / 触碰用户实例的东西** | **实证(2026-09-14 事故)**:为量内存**用 root 手动起 admin 的 profile** ⇒ 它以 root 写入 `home/storages/workspace.json`(0.1.5 新增的 `@deepseek-ai/dsh-workspace` 状态文件)与 `home/.dsh/mcn-plugin.db` ⇒ **属主变 root** ⇒ 实例进程(uid 114801)`EACCES` ⇒ `plugin tree failed to load` ⇒ **exitCode 1 崩溃循环 ⇒ 页面 404**。**规则**:① 对用户实例的**一切验证 / 冒烟 / 探针必须以该 uid 运行**(`setpriv --reuid <uid> --regid <uid> --clear-groups`)或**照平台姿势进 bwrap 沙箱**;⛔ **禁止 root 直跑 `dsh --profile`**。② 确需临时以 root 跑(读全局配置等)⇒ **收尾必须** `find <home> -user root` 列出 + `-exec chown <uid>:<uid> {} +` 修正。③ 实例「起不来」排查**先看属主 / EACCES**,**不要先怀疑 OOM**(本次先后误判为 OOM,绕了 20 分钟)。**修复手法(实测 1 步恢复)**:`find <home> -user root -exec chown <uid>:<uid> {} +` → 重启平台 → 页面 200。 |
| **R11** | **⛔ 只做正向迭代:命中「劣化风险」→ 立即停下复盘;确实无正向做法 → 立即停止,禁止继续执行**(2026-09-15 用户明令) | **判据(每次决策前过一遍十维)**:这个改动是否让项目**任一维度净变差** —— **目标 / 方向 / 架构 / 功能 / 性能 / 安全 / 交互 / UI / 便利性 / 扩展性**?<br>**命中 ⇒ 立即停下复盘**(不许"先做着看"、不许将就):① 写清**劣化在哪一维、代价多大**(证据 / 量级)② 找出**能保住正向收益的做法**(改小范围 / 换实现 / 分阶段)③ **拿不出正向做法 ⇒ 立即停止、不再执行,只报告**。<br>⛔ **三种伪装禁止**:把劣化说成"必要代价"/用"后续再优化"掩盖已知劣化/把劣化项藏进交付不写。<br>与 **R5**(权限只准收窄)互补 —— R5 管**权限**,R11 管**全维度净收益**;与 **U27**(不将就妥协)同源。 |
---
## 4. 提交边界
**未明确要求 → 不 commit / 不 push / 不同步仓库。** 用户说"提交 / 推送 / 同步"时才做,且**只 add 自己改的文件**。
---
## 5. 规划与执行分离
规划会话**只产出交接单**(`dsh-server-docs/交接单/`,8 段必填:目标 / 只读前置 / 范围 / 决策点 / 步骤 / 验收 / 回滚 / 回报格式),**不 ssh、不改码、不重启、不 scp**;落地交另一个执行会话(**不读规划会话的上下文**)。
---
## 6. 并发纪律(本库多会话并行是常态)
- 共享文件**只用 Edit 做精确片段替换**(失败 = 天然冲突检测),**禁整文件 Write 覆盖**。
- **同一时刻只放一个执行会话**(并行度按**冲突域**定,不按任务数);规划会话在执行期间**只读、不 scp**。
- **三把锁,顺序固定**(本平台多会话并行的唯一防线):
| 序 | 锁 | 命令 | 管什么 |
|---|---|---|---|
| ① | **全局执行锁**(粗) | `bash dsh-server-docs/scripts/handoff-guard.sh --claim-exec "<你的会话名>"` | **同一时刻只允许一个执行会话**动「文档 / 代码 / 服务器」← **防"5 个会话同时改"靠的就是这一把** |
| ② | 单级占用锁(细) | `bash dsh-server-docs/scripts/handoff-guard.sh --claim <单号> "<会话名>"` | 这个单归谁做(供台账与占用声明) |
| ③ | 服务器侧操作锁 | `bash scripts/op-lock.sh claim <操作名> "<影响面:谁会被断、断多久>"` | 谁正在动**生产**(重启 / drain / 改 env·配额 / 铺插件 / 改 nginx·nft) |
**完工反序释放**:先 `--release <单>` / `op-lock.sh release`,最后 `--release-exec`。
- 🔍 **抢锁必须"校验结果",不能"看输出"**(2026-09-15 实证事故):把 `handoff-guard.sh --claim-exec` 的输出**用管道截尾**(`| grep` / `| tail`)时,**失败提示里也含关键词**(如"…必须 `--release-exec` 才算完成")⇒ `grep -q` 会**假命中** ⇒ 于是"以为抢到了"而**在无锁状态下改库**。
✅ **正确判据(二选一,缺一不可)**:① **检查退出码**(`if bash scripts/handoff-guard.sh --claim-exec "X"; then … ; fi`,不要接管道);② **复读 `交接单/.exec-lock/OWNER` 并断言等于自己的会话名**。
⛔ 事故版形态:`OUT=$(… --claim-exec … | grep 已持…)` —— **grep 吃掉了退出码,也吃掉了"抢不到"这个事实**。
- ⚠️ **「无锁」的正确读法 =「你快去抢」,不是「可以开工」** —— 2026-09-12 实证:两个会话把 guard 输出的「✓ 无全局锁」读成"环境干净"→ **同时改了本库**(无实际损害,属流程失效)。**看到"无锁" ⇒ 下一个动作就是 `--claim-exec`**;**抢到才是开工许可**。
- 强制层(可选启用):`scripts/lock-guard-hook.py` + `~/.workbuddy/settings.json` 的 hooks —— **无锁时直接拒写**(见 `04-调整方案/73`)。
- ⛔ **抢不到锁就是终点,不是待办**:**不得人工删锁、不得接管**(见 **R9**,2026-09-12 用户明令)。唯一合规动作 = 等持有者自己释放,或**报告用户、由用户本人处置**;AI 不得以任何理由替用户判断"那把锁已经可以删"。
- ✅ **锁只约束「写」,不约束「读」**(2026-09-12 用户问清):读文档 / 读代码 / 只读命令(`git status|log|diff`、`journalctl`、`ls`、`grep`、只读 ssh)**随时可做,不需要锁** —— 被挡期间照样可以查清事实再报告。
⚠️ 但**会改本地状态的命令不算"读"**:`git fetch` / `checkout` / `stash` / `reset` / `switch` 等一律要持锁(它们会写 `.git/refs` 或工作树)。
- 🔓 **释放时机 = 整个交付闭环走完,不是"改完文件就放"**:`回填台账 → 四件套校验 → commit → 推送 + 对账 → 归档` 全部结束后才 `--release-exec`(**反序**:先 `--release`,最后 `--release-exec`)。
理由:中间放锁 = 别的会话可能在你 commit 前挤进来,让**你的半成品被它的提交带走**(本库实证过这类事故)。
- ⏸️ **持锁期间若需要等用户拍板(等窗口 / 等选择)→ 先释放锁,再等**:锁是"**正在动手**"的凭证,不是"先占着"。挂着锁空转会把所有会话挡在门外;确认完再重新 `--claim-exec`。
- 🔒 **锁的生命周期 = 任务的生命周期**(2026-09-14 用户明令):**抢到锁的任务,只有"执行完成 → 反序释放"才算完成**;⛔ **禁止"抢到锁、做一半、不解锁就结束回合 / 结束会话"** —— 锁是**独占资源**,带锁结束 = 把其他所有会话挡在门外,而本库**无心跳机制**、别人**没有任何判据**能确认你已停 ⇒ 会被迫空等,或被诱去违规接管(R9)。
三条硬性配套:① **抢锁前先把收口步骤列出来**(落地 → 校验 → 推送/对账 → 收尾),别做到一半才发现收不完;② **中途必须停**(等用户拍板 / 等外部窗口)⇒ **先释放锁再停**(上一条);③ **结束语必须对锁状态负责** —— 要么写明"**已释放**",要么**显式点名"锁仍在 `<OWNER>`、未释放、原因、下一步"**(仅限"释放通道不可用"这类极端情形);⛔ **"忘了 / 做不完就走"一律不允许**。
- 推送前复跑对账:**「仅本地」里若有不在你清单里的文件 → 立刻停手**(幽灵文件);**只推自己本次改的文件**。
- 单子里的基线数字**必须带取数时间 + 复核命令**,不写死绝对值(会被并行改动打穿)。
- **`~/.workbuddy/settings.json` 的 `hooks` 段 = 多会话共享配置** —— 多个会话各自加钩子时**只能 Edit 增删条目,禁止整段覆盖**(JSON 顶层键被覆盖会**静默**抹掉别人的钩子)。
2026-09-12 实证:两处独立钩子(提问闸门 / 锁闸门)各写一份配置文档,若各自按文档落盘 → **互相覆盖**;已合并为一段(`PreToolUse` 两条 + `SessionStart` 一条)。
**同一条也适用于用户级 `~/.workbuddy/MEMORY.md`。**
- ⚠️ **钩子命令「会话启动时快照」** —— 改 `~/.workbuddy/settings.json` 里的 hook,**对已在跑的会话无效**,必须**「完全重启」(彻底退出——关窗 ≠ 退出)或新开会话**才加载。**2026-09-13 实测定论**(本会话 06:47 启动 → 06:55 改配置 → 07:05 拆掉临时目录联接后,写操作报的**仍是旧路径**;此前那版「每次调用现读」是被那支临时联接掩盖的**误判**)。
- ⚠️ **脚本路径失配 = fail-closed**:hook 打不开脚本 → 报错 → 该机**所有会话**的 Write/Edit 全被拒(09-13 实际发生,连改 `settings.json` 本身都被拦)⇒ **迁移 / 改名后第一件事 = 核对 hooks 里的绝对路径**。
- **应急兜底(本钩子有意的安全阀)**:钩子**不拦 Bash** ⇒ 路径失配期间可用 shell 写文件过渡(09-13 实际走通)。
---
## 8. 会导致事故的实测事实(**常驻,不许只给指针**)
| 事实 | 不知道会怎样 |
|---|---|
| **实例权限档位是「会话创建时播种」的** —— 既有会话**不跟随平台默认**(平台默认 `danger-full-access`) | 会误判"平台坏了";更危险的是可能**自动去改档位** —— 那等于把受限会话静默提升为完全权限,**安全语义变更必须用户知情** → 只提示 + 建议新开会话 |
| **功能插件「禁用」= `pnpm remove`(真卸载)**,不是"保留包 + disabled" | 任何**硬绑定 provider** 的覆写段在用户禁用后会指向不存在的 provider;`web.searchProvider` **单选且无回落**,多 provider 又未显式配置 → `WEB_PROVIDER_AMBIGUOUS` 报错(平台已用 `syncWebProviderPatch()` 按当前 bundles 重算解决) |
| **实例配额** = `MemoryMax 384 MiB` / `CPUQuota 150%` / `TasksMax 128`;但 **V8 堆上限按宿主物理内存算(960 MiB)而非 cgroup** | ① `systemd` 的 `MemoryCurrent`/`MemoryMax` **单位是字节、不是 KB**(按 KB 算会放大 1024 倍)② 不注入 `NODE_OPTIONS=--max-old-space-size=256` → 实例会先撞**内核 SIGKILL**(无优雅退出、无日志,排查时无从下手) |
| **业务功能插件(第三方 + 自研)只有一种投放方式** = 「**admin 在门户导入候选池 → 用户在实例「功能管理」里自己启用/禁用**」(用户 2026-09-12 明确:三方插件**一律**按这个处理,**不需要铺什么**) | 若图省事改"直铺"(直接往用户 profile 装包 + 写 provider 覆写段),后果有三:① **绕过用户自决** —— 用户看不到、也关不掉;② 直铺覆写段与档案 65 的平台托管段**同属"整体替换 config"语义** → 两段并存**互相覆盖**,产生**难察觉的配置漂移**;③ 与托管段机制重复建设。**无例外** —— 连 AnySearch 最初走的直铺,也已由用户拍板改回候选池(档案 64 §8.3 修正)。⚠️ **例外只限平台基础设施插件**(`portal-entry` / `business-plugins` / `workspace-scoped-picker`):它们仍是平台级直铺、用户无感,见档案 16「插件三层归属」 |
> 细节与当时实测:`04-调整方案/33`(权限档位)· `04-16`/`04-64`/`04-65`(插件与 provider)· `04-58`(内存与配额)。
---
## 待核清单(换环境后逐项核对,脚本不代改)
- [ ] **工作区根** E:/ProgramData/AI技能/aliyun-dsh-server
- [ ] **文档库** D:/github/dsh_shenxian/dsh-server-docs
- [ ] **代码仓** D:/github/dsh_shenxian
- [ ] **备份目录** /opt/dsh/backups
- [ ] **服务器** bt-server(47.77.182.89,SSH 32022)
@@ -0,0 +1,228 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""resident-rules.py —— 「常驻规则」的可移植化:快照 / 校验 / 注入 / 环境自检
为什么需要它
────────────
项目常驻规则写在**工作区**的 `CODEBUDDY.md`(每会话自动注入 ⇒ 动作前必然生效)。
但它是**工作区文件**:换电脑 / 换工作区路径 ⇒ 规则直接消失,而"技能"才是**可移植层**。
本脚本 + 同目录 `references/常驻规则-快照.md` 把两者接起来:
CODEBUDDY.md(工作区·权威·动作前生效)
│ --snapshot(抽取 §1/§3/§4/§5/§6/§8 全文)
▼
references/常驻规则-快照.md(技能内·可移植·随技能走)
│ --check / --inject(到新环境的 CODEBUDDY.md)
▼
新环境的 CODEBUDDY.md
设计红线(避免造"第二真相源")
──────────────────────────────
1. **权威方向单向**:`CODEBUDDY.md` 是权威,快照是**它的副本**(`--snapshot` 生成,不手改)。
2. **默认只报不改**:`--check` 只报告差异(退出码 1)—— 与文档库「体检只报不改」同规。
3. **注入只动标记块**:`--inject` 只替换 `<!-- BEGIN resident-rules … --> … <!-- END -->` 之间的内容;
首次注入**不自动建块**(除非 `--init`),避免与人工撰写的内容重复成两处。
4. **区分环境无关 / 环境相关**:环境相关项(绝对路径 / IP / hooks 路径 / 工具位置)
注入后**必须按新环境核对** —— 脚本给「待核清单」,不假装能自动配好。
用法
────
python3 resident-rules.py --snapshot # 由 CODEBUDDY.md 重生成技能内快照
python3 resident-rules.py --check [--goal <CODEBUDDY.md>] # 校验目标环境(默认只报,rc=1 = 有差异)
python3 resident-rules.py --env-check # 环境自检:路径 / hooks / 锁脚本 / 工作区
python3 resident-rules.py --inject [--goal <CODEBUDDY.md>] [--init] # 注入(需已存在标记块)
退出码:0 = 一致/成功;1 = 有差异(未改任何文件);2 = 用法/环境异常
"""
import io
import json
import os
import re
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
SKILL = os.path.dirname(HERE)
SNAP = os.path.join(SKILL, 'references', '常驻规则-快照.md')
DEFAULT_GOAL = r'E:/ProgramData/AI技能/aliyun-dsh-server/CODEBUDDY.md'
# 要随技能走的章节("动作前必须生效"的那些 + 事故级事实)
SECTIONS = ['## 1. 提问判据', '## 3. 红线', '## 4. 提交边界', '## 5. 规划与执行分离',
'## 6. 并发纪律', '## 8. ']
# ★骨架锚点:这几条丢了就等于规则没了 —— 校验时逐条查存在性(短语取自权威文件)
ANCHORS = [
('A1', '只问「超过现有判断方法边界」的问题'),
('A2', '禁止用征询句收尾'),
('A3', '四件或七件套必跑', '占位'), # 由下方 canary 覆盖
('R7', '禁未经确认的批量 / 全仓写入'),
('R7-边界', 'R7 只适用于「不是我的 lane」'),
('R9', '绝对禁止「人工删锁 / 接管」'),
('R11', '只做正向迭代'),
('U27', '目标不打折,路径取最小代价'),
('L1', '先抢全局执行锁'),
('L2', '锁的生命周期 = 任务的生命周期'),
]
MARK_BEGIN = '<!-- BEGIN resident-rules (generated by skills/dsh-env-bootstrap · 勿手改块内) -->'
MARK_END = '<!-- END resident-rules -->'
# 环境相关项:注入/换机后**必须按新环境核对**(不是"自动配好")
ENV_ITEMS = [
('工作区根', r'E:/ProgramData/AI技能/aliyun-dsh-server'),
('文档库', r'D:/github/dsh_shenxian/dsh-server-docs'),
('代码仓', r'D:/github/dsh_shenxian'),
('备份目录', r'/opt/dsh/backups'),
('服务器', 'bt-server(47.77.182.89,SSH 32022)'),
]
def rd(p):
try:
return io.open(p, encoding='utf-8', newline='').read()
except OSError:
return ''
def section_of(text, head):
"""取 `head` 章节到下一个同级 `## ` 之间的内容(含标题行)。"""
i = text.index(head)
m = re.search(r'^## ', text[i + len(head):], re.M)
return text[i:i + len(head) + (m.start() if m else len(text) - i - len(head))].rstrip() + '\n'
def snapshot(src):
t = rd(src)
if not t:
raise SystemExit('ERROR: 读不到源文件 %s' % src)
parts = []
for h in SECTIONS:
if h == '## 3. 红线':
h = '## 3. 红线 R1' # 实际标题含编号,用前缀匹配
i = t.find(h)
if i < 0:
continue
j = t.find('\n## 4.', i)
parts.append(t[i:j if j > 0 else len(t)].rstrip() + '\n')
continue
if t.find(h) < 0:
parts.append('<!-- 源文件里没有 `%s` 章节 -->\n' % h)
continue
parts.append(section_of(t, h))
body = '\n---\n\n'.join(parts)
head = ('# 常驻规则 · 可移植快照(由 `scripts/resident-rules.py --snapshot` 生成,勿手改)\n\n'
'> **这是「工作区 `CODEBUDDY.md` 关键章节」的副本**,随技能走 ⇒ 换电脑 / 换工作区也能带走。\n'
'> ⚠️ **权威方向单向**:`CODEBUDDY.md` 是权威,本文件是它的副本(重生成用 `--snapshot`)。\n'
'> ⚠️ **环境相关项**(绝对路径 / IP / hooks 路径 / 工具位置)注入后**必须按新环境核对**:见文末「待核清单」。\n\n'
'## 骨架锚点(校验用;丢了就等于规则没了)\n\n')
anch = '\n'.join('- `%s` — %s' % (k, v) for k, v, *_ in
[(a[0], a[1]) for a in ANCHORS]) + '\n'
tail = ('\n---\n\n## 待核清单(换环境后逐项核对,脚本不代改)\n\n'
+ '\n'.join('- [ ] **%s** %s' % (k, v) for k, v in ENV_ITEMS) + '\n')
out = head + anch + '\n---\n\n' + body + tail
os.makedirs(os.path.dirname(SNAP), exist_ok=True)
io.open(SNAP, 'w', encoding='utf-8', newline='\n').write(out)
return len(out), len(parts)
def check(goal):
t = rd(goal)
if not t:
print('ERROR: 读不到目标 %s' % goal)
return 2
snap = rd(SNAP)
if not snap:
print('ERROR: 缺少技能内快照(先跑 --snapshot)')
return 2
print('=== 常驻规则校验|目标 = %s ===' % goal)
bad = 0
for a in ANCHORS:
k, phrase = a[0], a[1]
if phrase.startswith('四件或七件套'):
ok = ('七件套' in t) or ('四件套' in t)
else:
ok = phrase in t
print(' %-10s %s %s' % (k, '✓' if ok else '✗ 缺失', phrase[:40]))
bad += 0 if ok else 1
sec_ok = sum(1 for h in ('## 1. 提问判据', '## 3. 红线', '## 4. 提交边界',
'## 5. 规划与执行分离', '## 6. 并发纪律') if h in t)
print(' 章节存在性:%d/5' % sec_ok)
bad += 5 - sec_ok
print('结论:%s' % ('✅ 关键规则齐备' if bad == 0 else '⚠️ 有 %d 处缺失/漂移 ⇒ 用 --inject(或人工补齐)' % bad))
return 1 if bad else 0
def env_check():
print('=== 环境自检(换机后逐项确认)===')
bad = 0
for k, v in ENV_ITEMS:
if k in ('服务器', '备份目录'):
print(' %-8s %-52s (需 ssh 侧核对)' % (k, v))
continue
ex = os.path.exists(v)
print(' %-8s %-52s %s' % (k, v, '✓ 存在' if ex else '✗ 不存在'))
bad += 0 if ex else 1
# hooks 里的绝对路径(工作区搬迁后最常见的坑)
st = r'E:/ProgramData/.workbuddy/settings.json'
if not os.path.exists(st):
st = os.path.expanduser('~/.workbuddy/settings.json')
try:
h = json.loads(rd(st)).get('hooks', {})
except Exception:
h = {}
cmds = [x.get('command', '') for v in h.values() for it in v for x in it.get('hooks', [])
if x.get('type') == 'command']
print(' hooks 命令 %d 条:' % len(cmds))
for c in cmds:
paths = re.findall(r'[A-Za-z]:/[^\s"]+?\.(?:py|sh|exe)', c)
miss = [q for q in paths if not os.path.exists(q.replace('/', os.sep))]
flag = '✗ 路径失效' if miss else '✓'
print(' %s %s' % (flag, c[:100]))
bad += 1 if miss else 0
return 1 if bad else 0
def inject(goal, init=False):
t = rd(goal)
if not t:
print('ERROR: 读不到目标 %s' % goal)
return 2
snap = rd(SNAP)
if not snap:
print('ERROR: 缺少技能内快照(先跑 --snapshot)')
return 2
body = snap.split('## 待核清单', 1)[0]
block = MARK_BEGIN + '\n' + body.strip() + '\n' + MARK_END + '\n'
if MARK_BEGIN in t:
new = re.sub(re.escape(MARK_BEGIN) + r'.*?' + re.escape(MARK_END) + r'\n?',
block, t, flags=re.S)
io.open(goal, 'w', encoding='utf-8', newline='').write(new)
print('✓ 已替换标记块内内容(块外人工内容未动)')
return 0
if not init:
print('⚠️ 目标里没有标记块 ⇒ **不自动注入**(避免与人工撰写的内容重复成两处)。\n'
' 要建立标记块并注入:加 --init(会**追加**到目标文末,不动既有内容)')
return 1
io.open(goal, 'w', encoding='utf-8', newline='').write(t.rstrip('\n') + '\n\n' + block)
print('✓ 已追加标记块(既有内容未动);建议随后人工去重并 `--check`')
return 0
def main(argv):
goal = DEFAULT_GOAL
if '--goal' in argv:
goal = argv[argv.index('--goal') + 1]
if '--snapshot' in argv:
n, k = snapshot(goal)
print('✓ 已由 %s 生成快照:%d 字符 / %d 个章节 → %s' % (goal, n, k, os.path.relpath(SNAP, SKILL)))
return 0
if '--env-check' in argv:
return env_check()
if '--inject' in argv:
return inject(goal, init=('--init' in argv))
if '--check' in argv:
return check(goal)
print(__doc__)
return 2
if __name__ == '__main__':
raise SystemExit(main(sys.argv[1:]))