Files
workbuddy_skills/session-mechanism/references/作业规矩/02-工作区纪律.md
T

269 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 参考 02 — 工作区纪律(全文)
> 本文件是 `agent-operating-rules` 的**细节层**。入口已给硬规则;本文件给**完整依据、出处与全表**。**按需读**。
---
## 1. 🔴 工作区归属三律(**最优先,违反即事故**)
**背景实测**:某工作区会话「参考接续会话的规则」时,**照抄了另一个工作区的绝对路径** ⇒ 把**入口文件**和**接续任务**都落到了别人的工作区,而它那条线自己的家是第三处。同一份入口出现两处、md5 相同 = **第二真相源**;平台工作区的状态脚本因此把**别线**报成了自己的线(实测报"2 条工作线",其中一条不是它的)。
| 律 | 内容 | 反例(都真发生过) |
|---|---|---|
| ① **入口只允许一份** | 位置 = **那条线自己的工作区根**;头部必须写 `> 🔴 **工作区**:<该工作区绝对路径>` | 两处同改(两个工作区各一份,md5 相同) |
| ② **自动化 `cwds` = 本工作区** | 定时 / 接续任务的 `cwds` 只认**那条线自己的工作区**;⛔ 不因"脚本在别的工作区"就把 `cwds` 设过去 | 照抄"第 0 步跑 `state.py`" ⇒ `cwds` 写成**脚本所在**的工作区 |
| ③ **参考规则 = 加载技能,不是读别的工作区的文档** | 要副本就复制**规则文档**到本工作区 `docs/会话与接续/`(或等价目录);⛔ **入口文件不复制** | 直接读别的工作区的 `接续入口_*.md`,照抄其中路径 |
**跨工作区取脚本的正确姿势**(不违反律②)—— 本工作区没有脚本时,用**绝对路径**调、并指定目标工作区:
```bash
python "<脚本绝对路径>" --ws "<自己的工作区绝对路径>"
```
⛔ **把脚本复制到每个工作区** = 多一份要维护的代码;`--ws` 是正解。
⚠️ 若脚本**不支持 `--ws`**,仍然用绝对路径调用它,**并额外用本项目自己的方式取状态** —— ⛔ 不要为了"对齐输出格式"而把脚本抄一份过来。
### 1.1 收尾自检一句
> **本棒的 `cwds` 是否 = 我这条线自己的工作区?入口文件是否只在我这个工作区存在一份?**
### 1.2 发现自己正在越界时怎么办
- **只读**别的工作区:允许,但要意识到**它的结论不是本工作区的结论**。
- **写入**别的工作区:⛔ **停手**,先报告 —— 除非用户明确要求。
- 已经在别处留了副本:**先报告清单**(哪份文件、在哪两处、md5 是否相同),**取得确认后再删副本**(删除不可逆)。
---
## 2. 并发纪律:一把锁,顺序固定,反序释放
> 多会话并行是常态。**「无锁」的正确读法 =「你快去抢」,不是「可以开工」**(实测:两个会话把「✓ 无全局锁」读成"环境干净" ⇒ 同时改了同一批文件)。
### 2.1 取锁(**改任何文件之前的第一步,不是"检查"是"抢"**)
```bash
bash "<锁脚本绝对路径>" --claim-exec "<你的会话名>"
```
- **抢到之前不要动任何文件**;**抢不到 = 有会话在跑 ⇒ 停手 + 报告**。
- 细粒度锁(如果项目有):先占单 → 再动单内的文件。
- 生产侧操作锁(如果项目有):重启 / 停任务 / 改配置 / 铺包 / 改网关 → 先声明**影响面**(谁会被断、断多久)。
- 完工:先放细粒度锁 / 操作锁,**最后** `--release-exec "<会话名>"`(⛔ 不带会话名 ⇒ **拒绝释放**;且护栏额度耗尽时会被 `safe-delete` 拦住 ⇒ 见 §2.3)。
### 2.2 🔍 抢锁必须"校验结果",不能"看输出"(**实测事故**)
把抢锁命令的输出**用管道截尾**(`| grep` / `| tail`)时,**失败提示里也含关键词**(如"…必须 `--release-exec` 才算完成")⇒ `grep -q` 会**假命中** ⇒ 于是"以为抢到了"而**在无锁状态下改文件**。
✅ **正确判据(二选一,缺一不可)**:
① **检查退出码**(不要接管道):
```bash
if bash "<锁脚本>" --claim-exec "X"; then ... ; fi
```
② **复读锁文件里的 `OWNER` 并断言等于自己的会话名**。
⛔ 事故版形态:`OUT=$(… --claim-exec … | grep 已持…)` —— **grep 吃掉了退出码,也吃掉了"抢不到"这个事实**。
### 2.3 释放时机 = **整个交付闭环走完**,不是"改完文件就放"
`回填台账 → 校验 → commit → 推送 + 对账 → 归档` 全部结束后才释放(**反序**)。
理由:中间放锁 = 别的会话可能在你 commit 前挤进来,让**你的半成品被它的提交带走**。
- 🔴 **护栏额度耗尽时的等效释放(2026-09-25 实测)**:`safe-delete` 批量护栏用尽后,任何 `rm -rf` 都会被拦住并**挂起至超时** ⇒ 连**自己的锁**也放不掉(`--release-exec` 返回 **rc=124**)。处置 = **`mv` 把锁目录改名移走**(走 ENOENT 快路径、零删除),并在回报里写明理由。⚠️ 护栏阈值变量真名 = `CODEBUDDY_SAFE_DELETE_BULK_GUARD`(⛔ 不是 `…_BULK_THRESHOLD`);护栏**按会话计数** ⇒ 新会话额度是满的。
### 2.4 三条硬配套
- ⏸️ **持锁期间若要等用户拍板 ⇒ 先释放锁,再等** —— 锁是"**正在动手**"的凭证,不是"先占着"。
- 🔒 **锁的生命周期 = 任务的生命周期**:⛔ 禁止"抢到锁、做一半、不解锁就结束回合 / 结束会话" —— 带锁结束 = 把其他所有会话挡在门外。
- 🗒️ **结束语必须对锁状态负责**:要么写明"**已释放**",要么**显式点名"锁仍在 `<OWNER>`、未释放、原因、下一步"**(仅限释放通道不可用的极端情形)。⛔ "忘了 / 做不完就走"一律不允许。
### 2.5 ⛔ 绝对禁止「人工删锁 / 接管」
- AI 一律不得:删锁目录、删占用标记、或以「持有者疑似已死 / 卡住 / 太久没动 / 只在只读分析没产出」为由**单方面接管**。
- 锁**只能由持有者自己释放**;脚本输出里的「或确认接管后人工删锁」**不构成授权**。
- 抢不到锁时**唯一**合规动作 = **停手 + 报告用户** —— **锁的处置权只属于用户本人**。
- **理由**:删锁 = 在**无法验证**对方死活的前提下单方面撤销互斥(**无心跳机制**)⇒ 一旦对方仍在跑,就退回「两个会话同时改同一批文件」,而这正是这把锁存在的理由。
### 2.6 ✅ 锁只约束「写」,不约束「读」
读文档 / 读代码 / 只读命令随时可做。
⚠️ 但**会改本地状态的命令不算"读"**:`git fetch` / `checkout` / `stash` / `reset` / `switch` 一律要持锁(它们会写 `.git/refs` 或工作树)。
### 2.7 ⚠️ 细粒度锁管不住跨单撞车
多个会话各做各的单时,单级锁互不冲突,但**都会改共享文件**(清单 / 台账 / 索引)⇒ **只有全局锁能串行化**。
---
## 3. 目录落位与命名
> 判据:**新增任何文件前,先问"它属于哪个工作区、哪一类"**,答不出来就先去问规则文件,⛔ 不要随手写在根目录。
### 3.1 通用落位形态(具体目录名以本工作区规范为准)
| 手上是什么 | 放哪 |
|---|---|
| 正式文档(方案 / 报告 / 规范 / 复盘) | `docs/<主题>/` |
| 交给下一棒的交接单 | `交接单/`(或项目约定的台账目录) |
| **工作线入口**(每个会话第一个读的) | **工作区根**,命名 `接续入口_<线名>_<日期>.md` |
| 一次性脚本、探针输出、中间证据 | `tmp/<任务名>-<日期>/` |
| 过程目录(一个任务一整个目录) | `tmp/历史过程目录/` |
| 不再引用但需留痕 | `归档/` |
| 疑似可删 | `待清理/`,**列清单等确认** |
### 3.2 ⚠️ 入口文件**必须在根**(会出事故)
状态脚本通常用 `listdir(工作区根)` 扫描 `接续入口_*.md`。**移走 ⇒ 新会话读到的第一个信号就是错的**(事故级)。
⇒ 入口文件**不允许**被"整理"进子目录。
### 3.3 命名
| 类型 | 规则 | 例 |
|---|---|---|
| 正式文档 | `<主题>_<YYYYMMDD>.md` | `集群化改造方案_20260914.md` |
| 交接单 | `交接单_<主题>_<日期>.md` | `交接单_组密钥加密_20260918.md` |
| 线入口 | `接续入口_<线名>_<日期>.md` | 放根 |
| 临时脚本 / 输出 | `_<用途>.<ext>` | `_probe_instance_mem.sh` |
| 过程目录 | `_tmp_<序号或主题>/` | `_tmp_seq41/` |
日期一律 **8 位 `YYYYMMDD`**,不加分隔符。
### 3.4 ⚠️ 改写文档内的引用路径时
**映射键必须收敛到「带日期戳」的文件名** —— 通用名(`README.md` / `INDEX.md` / `architecture.md`)在任何文档里都可能指别处,映射它**必然误伤**。
### 3.5 路径书写约定
- ⛔ **禁止"跨目录只写文件名"** —— 从别处引用子目录文件时必须写**相对该引用点可定位**的路径。
- 占位符用尖括号 `` `<NN>-<主题>.md` ``,⛔ 不用 `NN-*`(易被误认为真实路径)。
- 路径一律反引号包裹,便于机器扫描与跳转。
### 3.6 删除一律不可逆
⇒ 先移入 `待清理/`,**出清单 + 取得确认**后才真删。`待清理/` 非空时,收尾报告需提一句它还剩什么。
### 3.7 分层判据(防止把规则做成指针)
> **"不看会违规 / 会出事"的 → 写成实体内容;"看了更准但不看也不违规"的 → 才给指针。**
⇒ 红线、判据、环境陷阱**必须常驻**;平台背景知识、UI 细节、档案模板、历史方案**可以只给指针**。
---
## 4. 提交边界
> **未明确要求 → 不 commit / 不 push / 不同步仓库。** 用户说"提交 / 推送 / 同步"时才做,且**只 add 自己改的文件**。
- ⛔ **三类内容通常禁入库**:**临时目录**(`tmp/`)、**中间产物**(`_tmp*/`、`_中间产物_*/`)、**会话交接单**(若项目约定它们只作本地台账)。用 `.gitignore` 兜住。
- 🔴 **不要用 `git status` 判"交接单要不要提交"** —— 被 ignore 之后它们**根本不会出现**在 status 里;这是本条禁令的**预期行为**,不是"没生成"。确需入库只能显式 `git add -f`,且**先说明理由**。
- ✅ **落点与入库解耦**:交接单**照旧写到约定目录**(路径不变,同一处找得到),只是**不进 Git**、以本地未跟踪文件形态保留。
- 🔴 **提交前自查**:`git diff --cached --name-only` 里出现禁入类 ⇒ **立即 `git reset` 撤出**。
- ⚠️ **推送前必须对账**:「仅本地」里若有**不在你清单里的文件 → 立刻停手**(幽灵文件);**只推自己本次改的文件**。
- ⚠️ **核验"推送是否到位"用 `git ls-remote origin refs/heads/<branch>`**(与本地 `git rev-parse --short HEAD` 对比)。⚠️ 本机可能**没有 remote-tracking ref** ⇒ `git log origin/main..HEAD` 直接报 `unknown revision`,**别把它的空输出当成"已全部推送"**。
- ⚠️ **同一条纪律适用于共享配置文件**(工具配置的 `hooks` 段、用户级记忆文件等):**只能 Edit 增删条目,禁止整段覆盖** —— 顶层键被覆盖会**静默**抹掉别人的配置。
- ⚠️ **脚本路径失配 = fail-closed**:钩子打不开脚本 ⇒ 该机**所有会话**的写操作全被拒 ⇒ **迁移 / 改名后第一件事 = 核对钩子里的绝对路径**。
---
## 5. 批量操作红线
- ⛔ **禁止未经确认的批量 / 全仓写入**:全库遍历改写(`os.walk` / `find -exec` / `grep -rl | xargs`)、通配符重写、批量 `chmod` / `chown`、**批量换行符转换(CRLF↔LF)**、`cp -r` 整目录覆盖、`git add -A`。
- **任何可能影响 >10 个文件的操作,用前必须先出受影响清单并取得确认**;先用 1 个对象**单点验证**,确认后果符合预期再推广。
- **只做被明确要求的事**:执行过程中发现的额外问题——哪怕看起来"很小、很好修"——一律**先报告、后动手**。用户说"按建议处理"只授权**那条建议本身**,不等于授权一切顺带优化。
- 🔑 **判据看"归属",不看"是不是平台组件"**:
**本项目自己的**资源(自己的服务器单元、自己的数据目录、自己的网关配置、自己的端口)⇒ **按本项目规则直接做**,动手前一句话说明即可。
**别人的 / 归属不明**的对象 ⇒ **一律只报告、不动手**,**哪怕改它能让自己流程跑通**。
- ⚠️ **"我 lane 内的执行细节"不算批量越界**:部署 / 上线(换包、传产物、改静态页、投放)、重启自己的服务、改自己的配置、跑自己的脚本 ⇒ **别拿"批量红线"当挡箭牌去问,直接做**,事后一句「我选了什么(可推翻)」。
- 🔑 **本机副本不是沙箱**:本机的批量改动即便不带任何"部署"动作,也会在下一次同步时**传导到生产**。传播前必须用 `git status` 确认**待传清单只含本次真实改动**。
---
## 6. 环境陷阱清单(通用形态,**具体路径以本工作区为准**)
| 陷阱 | 表现 | 正确做法 |
|---|---|---|
| **PATH 被削** | `ls` / `grep` / `dirname` / `head` 全部 `command not found`,报错里出现 `cd: null directory` | 每条命令**显式前置** PATH(把工具链的 `usr/bin` 与 `mingw64/bin` 都加回去),⛔ 不要指望 shell 继承 |
| ⛔ **别把系统目录前置进 PATH** | 某些环境里那里的 `bash` 是**另一个子系统的启动器** ⇒ 只剩乱码报错 | 用工具链自带的 POSIX 目录,**不要加系统盘目录** |
| **沙箱可能拦某个程序** | 报 "PROGRAM BLOCKED BY SECURITY POLICY" | ⛔ **不要重试、不要绕道调用**(换 shell / 写脚本都不行);改用手上可用的等价手段,并向用户说明 |
| **行尾(CRLF / LF)** | 本机工作副本是 CRLF、目标环境是 LF ⇒ 直接传会污染生产 | 只转**本次要传的那一个**文件(`tr -d '\r' < 源 > /tmp/x` 再传);判据用 **字节级**(数 `\r` / `od -c`),⚠️ **别用 `grep -c $'\r'`**(在 git bash 里会误报) |
| **运行时版本错配** | 原生模块报 `ERR_DLOPEN_FAILED` / `NODE_MODULE_VERSION` 不符 ⇒ **看起来像"我改坏了",其实是环境** | 跑单测 / 验收用**项目要求的那个版本**(显式绝对路径调用) |
| **依赖隔离** | 全局装包 ⇒ 污染用户环境 | 用项目约定的隔离方式(venv / 本地 `node_modules`);用绝对路径调用解释器 |
| ⚠️ **`python -c` 内含引号** | 转义地狱 | 先落成 `.py` 文件再跑 |
| ⚠️ **语法检查落盘污染对账** | 语法检查命令**必然**在脚本旁落 `__pycache__/*.pyc` ⇒ 被对账脚本算成"待推送" | 用**不落盘**写法(`ast.parse`);落了就清掉并**复跑对账清零** |
| ⚠️ **含反引号 / 特殊字符的命令** | 命令行转义不可控 | 用写文件的方式(Write / Edit)落命令,再执行 |
| ⚠️ **改表格行:别按整行文本匹配** | 想给某行追加说明,用「整行字符串相等」定位会**静默失配**(实际引号字符 / 全半角与脚本内不同)⇒ 改了但没写进去 | **按行号或行首前缀锚定**(`ln.startswith("| `sk-xxx/`")`),改完**打印命中行号**自证 |
| ⚠️ **对账脚本的默认远程可能失效** | 脚本写死某 ssh 别名(或调用 ssh 时**没带非默认端口**),而该别名端口已改 ⇒ 直接跑必然报「无法读取服务器目录」,**看起来像服务器挂了** | 先用 `ssh -p <真实端口> <别名> 'echo OK'` **单独探连通**;再用脚本的 **环境变量 override**(如 `DOCS_REMOTE=user@ip`)跑;⛔ 脚本没改就别顺手改,记入待办 |
### 6.1 🔴 本机铁律:绝不以 root(或非该资源所属 uid)运行 / 触碰别人的东西
**实测事故**:为量内存**用 root 手动起某个实例的 profile** ⇒ 它以 root 写入状态文件 ⇒ **属主变 root** ⇒ 实例进程 `EACCES` ⇒ **崩溃循环 ⇒ 页面 404**。
**规则**:
① 对用户实例的**一切验证 / 冒烟 / 探针必须以该 uid 运行**(`setpriv --reuid <uid> --regid <uid> --clear-groups`)或按平台姿势进沙箱;⛔ **禁止 root 直跑**。
② 确需临时以 root 跑 ⇒ **收尾必须** `find <目录> -user root` 列出 + `-exec chown <uid>:<uid> {} +` 修正。
③ 「起不来」排查**先看属主 / `EACCES`**,**不要先怀疑内存**(实测先后误判为内存问题,绕了 20 分钟)。
### 6.2 四条取数坑
① `pkill -f` 匹配**实际 argv**(不是你以为的模式)⇒ 定位进程用 `ss -lntpH 'sport = :PORT'`;
② 某些取数工具的 `fetch` **静默丢 `Host` 头** ⇒ 假 404 / 假 200 ⇒ 用 `curl -H "Host: …"`;
③ 本机与远端 `md5sum` 输出格式不同(`hash *path` vs `hash path`)⇒ 先 `cut -d' ' -f1`;
④ `journalctl --since` **不吃 ISO 偏移格式** ⇒ 用 `--since @<epoch>`;且"查询失败"与"确无该行"**必须可分**(⚠️ `| grep … || true` 会把"上游失败"伪装成"无命中")。
### 6.3 🔴 多处副本同步:**先判方向,再推**(实测踩过)
**起因(真实事故)**:某个"本机 ↔ 中继仓 ↔ 服务器镜像"三处链路,我**默认"本机最新"就批量从中间那处推远端** ⇒ 结果把**滞后的中间副本**推了上去。事后全量 md5 比对才发现:**6 个文件本机 ≠ 中间仓 = 远端**,且**本机版本号普遍更高**(例:`2.10.8` vs 中间仓的 `1.7.8`)。⇒ 中间仓与远端**长期滞后**,我的"同步"其实是**倒退**。
**判据(推任何多处副本之前,按序做三步)**:
1. **先全量比对**(一次跑完,别一个个看):逐文件算 md5,列出三方矩阵 ⇒ 看清是「三方一致」还是「某一方偏离」。
2. **判方向 = 比 `version:` 字段或 mtime**,⛔ **不是**比"谁在我手边"。`version` 更可靠(mtime 会被 copy 保留 `-p` 而失真)。
3. **只推确实滞后的那些**(先出清单再动手);⛔ 不要"整目录覆盖" —— 那会把方向搞反的代价放大到全部文件。
**⚠️ 关键认知**:多处副本场景里,**"本机"不等于"最新"**(别人可能已经改过远端);**"中间仓"也不等于"权威"**(它可能只是个滞后的归档)。**权威由 `version` / mtime / 内容共同决定,不由路径决定。**
**批量同步的硬闸**:任何"整目录 / 全量覆盖式"同步都属**批量操作**(见 §5)⇒ **先出"谁滞后"清单**,确认后才推。
### 6.4 🔴 批量 `scp` **同名文件**到同一目录 ⇒ 互相覆盖,只剩最后一个(2026-09-22 实测)
**症状**:把 12 个技能各自的 `SKILL.md` 一次 `scp` 到同一个暂存目录:
```bash
scp a/SKILL.md b/SKILL.md c/SKILL.md host:/tmp/push/ # ❌ 12 个文件同名
ls /tmp/push/ # 只有 1 个 SKILL.md
```
⇒ **同名 ⇒ 后传的覆盖先传的**,最终只剩**最后一个**的内容。
⚠️ 若下一步是"拿暂存目录里的文件去就位覆盖生产",就会**用 A 技能的内容覆盖 B 技能** —— 一次操作毁 11 个技能。
**为什么容易中招**:命令**返回码是 0**、不报错、不警告;只有去**数目标目录文件数**才发现。
**正解(二选一)**:
1. **逐文件推到各自路径**(推荐,最直白):`for f in ...; do scp "$f/SKILL.md" host:/dest/$f/SKILL.md; done`
2. **本地先按名组织目录树**再整树传:本地建 `push/<name>/SKILL.md` 后 `scp -r push/ host:/dest/`。
**通用判据(可推广到任何批量传输)**:
- 传前先问「**目标目录里会不会有重名**」—— 会 ⇒ 必须让**路径携带区分信息**(子目录),⛔ 不能只靠文件名。
- **传完必数文件数**(`ls | wc -l` 与预期比)。返回码 0 **不代表**数量对。
### 6.5 🔴 MSYS 路径(`/e/…`)⛔ **不许交给 Windows 原生程序**(2026-09-22 实测,正在增长)
**症状**:盘根长出**影子目录** —— 在 E 盘根出现 `E:\e\ProgramData\…`,与真 `E:\ProgramData\…` 并存。
**根因**:Git-Bash 风格路径 `/e/ProgramData/x` 交给 **Windows 原生程序**(`python.exe` / Chromium / node)后,
它按「**当前盘根 + 相对路径**」解释 ⇒ `e\ProgramData\x` ⇒ 当前盘是 E 就落到 **`E:\e\ProgramData\x`**。
**实测规模**:29 MB / 355 文件,含**本工作区自己的 hook 日志**(`stop-dialog-guard.log`,写入时间 = 当天 ⇒ **仍在增长**)
与浏览器 `_devlogs/<profile>/Cache`(Chromium user-data-dir)。
⚠️ **两个受害者同因**:hook 与浏览器自动化 —— 只要交出去的是 `/e/…` 形式就会中招。
**判据与修法**:
- **写盘前规范化**:`^/([A-Za-z])(/.*)?$` → `<大写盘符>:` + 余部(`/e/foo` → `E:/foo`);**幂等**(已是 Windows 形式则原样返回)。
- **修在入口**(取路径处包一层),不要在每个使用点打补丁。参考实现:本工作区 hook 的 `_norm_path()`。
- **自检**:`ls /<盘>/` 看有没有**单字母目录**(`e/` / `c/`);有 ⇒ 立即查是谁在写。