21 KiB
参考 02 — 工作区纪律(全文)
本文件是
agent-operating-rules的细节层。入口已给硬规则;本文件给完整依据、出处与全表。按需读。
1. 🔴 工作区归属三律(最优先,违反即事故)
背景实测:某工作区会话「参考接续会话的规则」时,照抄了另一个工作区的绝对路径 ⇒ 把入口文件和接续任务都落到了别人的工作区,而它那条线自己的家是第三处。同一份入口出现两处、md5 相同 = 第二真相源;平台工作区的状态脚本因此把别线报成了自己的线(实测报"2 条工作线",其中一条不是它的)。
| 律 | 内容 | 反例(都真发生过) |
|---|---|---|
| ① 入口只允许一份 | 位置 = 那条线自己的工作区根;头部必须写 > 🔴 **工作区**:<该工作区绝对路径> |
两处同改(两个工作区各一份,md5 相同) |
② 自动化 cwds = 本工作区 |
定时 / 接续任务的 cwds 只认那条线自己的工作区;⛔ 不因"脚本在别的工作区"就把 cwds 设过去 |
照抄"第 0 步跑 state.py" ⇒ cwds 写成脚本所在的工作区 |
| ③ 参考规则 = 加载技能,不是读别的工作区的文档 | 要副本就复制规则文档到本工作区 docs/会话与接续/(或等价目录);⛔ 入口文件不复制 |
直接读别的工作区的 接续入口_*.md,照抄其中路径 |
跨工作区取脚本的正确姿势(不违反律②)—— 本工作区没有脚本时,用绝对路径调、并指定目标工作区:
python "<脚本绝对路径>" --ws "<自己的工作区绝对路径>"
⛔ 把脚本复制到每个工作区 = 多一份要维护的代码;--ws 是正解。
⚠️ 若脚本不支持 --ws,仍然用绝对路径调用它,并额外用本项目自己的方式取状态 —— ⛔ 不要为了"对齐输出格式"而把脚本抄一份过来。
1.1 收尾自检一句
本棒的
cwds是否 = 我这条线自己的工作区?入口文件是否只在我这个工作区存在一份?
1.2 发现自己正在越界时怎么办
- 只读别的工作区:允许,但要意识到它的结论不是本工作区的结论。
- 写入别的工作区:⛔ 停手,先报告 —— 除非用户明确要求。
- 已经在别处留了副本:先报告清单(哪份文件、在哪两处、md5 是否相同),取得确认后再删副本(删除不可逆)。
2. 并发纪律:一把锁,顺序固定,反序释放
多会话并行是常态。「无锁」的正确读法 =「你快去抢」,不是「可以开工」(实测:两个会话把「✓ 无全局锁」读成"环境干净" ⇒ 同时改了同一批文件)。
2.1 取锁(改任何文件之前的第一步,不是"检查"是"抢")
bash "<锁脚本绝对路径>" --claim-exec "<你的会话名>"
- 抢到之前不要动任何文件;抢不到 = 有会话在跑 ⇒ 停手 + 报告。
- 细粒度锁(如果项目有):先占单 → 再动单内的文件。
- 生产侧操作锁(如果项目有):重启 / 停任务 / 改配置 / 铺包 / 改网关 → 先声明影响面(谁会被断、断多久)。
- 完工:先放细粒度锁 / 操作锁,最后
--release-exec "<会话名>"(⛔ 不带会话名 ⇒ 拒绝释放;且护栏额度耗尽时会被safe-delete拦住 ⇒ 见 §2.3)。
2.2 🔍 抢锁必须"校验结果",不能"看输出"(实测事故)
把抢锁命令的输出用管道截尾(| grep / | tail)时,失败提示里也含关键词(如"…必须 --release-exec 才算完成")⇒ grep -q 会假命中 ⇒ 于是"以为抢到了"而在无锁状态下改文件。
✅ 正确判据(二选一,缺一不可):
① 检查退出码(不要接管道):
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(" |
| ⚠️ 对账脚本的默认远程可能失效 | 脚本写死某 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)。⇒ 中间仓与远端长期滞后,我的"同步"其实是倒退。
判据(推任何多处副本之前,按序做三步):
- 先全量比对(一次跑完,别一个个看):逐文件算 md5,列出三方矩阵 ⇒ 看清是「三方一致」还是「某一方偏离」。
- 判方向 = 比
version:字段或 mtime,⛔ 不是比"谁在我手边"。version更可靠(mtime 会被 copy 保留-p而失真)。 - 只推确实滞后的那些(先出清单再动手);⛔ 不要"整目录覆盖" —— 那会把方向搞反的代价放大到全部文件。
⚠️ 关键认知:多处副本场景里,"本机"不等于"最新"(别人可能已经改过远端);"中间仓"也不等于"权威"(它可能只是个滞后的归档)。权威由 version / mtime / 内容共同决定,不由路径决定。
批量同步的硬闸:任何"整目录 / 全量覆盖式"同步都属批量操作(见 §5)⇒ 先出"谁滞后"清单,确认后才推。
6.4 🔴 批量 scp 同名文件到同一目录 ⇒ 互相覆盖,只剩最后一个(2026-09-22 实测)
症状:把 12 个技能各自的 SKILL.md 一次 scp 到同一个暂存目录:
scp a/SKILL.md b/SKILL.md c/SKILL.md host:/tmp/push/ # ❌ 12 个文件同名
ls /tmp/push/ # 只有 1 个 SKILL.md
⇒ 同名 ⇒ 后传的覆盖先传的,最终只剩最后一个的内容。 ⚠️ 若下一步是"拿暂存目录里的文件去就位覆盖生产",就会用 A 技能的内容覆盖 B 技能 —— 一次操作毁 11 个技能。
为什么容易中招:命令返回码是 0、不报错、不警告;只有去数目标目录文件数才发现。
正解(二选一):
- 逐文件推到各自路径(推荐,最直白):
for f in ...; do scp "$f/SKILL.md" host:/dest/$f/SKILL.md; done - 本地先按名组织目录树再整树传:本地建
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/);有 ⇒ 立即查是谁在写。