Files
STM32F4-Base/.dsh/skills/task-worktree/reference/pitfalls.md
T
wandering1 4a4ad6bc5f chore(全仓): @build/@test 旧文档别名清退为技能调用名
现状口径:/keil-build 与 /serial-boot-test 是 DSH 会话的技能调用名,
不是 shell 命令;「名 + 命令行参数」的命令形态改为可执行体
run_boot_test.py 的真实调用。

- AGENTS.md:别名说明重写为「技能调用名不是 shell 命令」,两条目改
  「编译(/keil-build)」「串口回归(/serial-boot-test)」
- keil-build / serial-boot-test 两 skill 调用名与示例同步;task-worktree
  方案文档去掉 @build/@test 惯例从句;@task 保留(不在点名范围)
- 123 文件现状陈述全改;台架捕获、变更记录/时间线、rename 映射输入
  等 41 行历史证据保留旧名
- gen_memory_svg.py 6 处措辞随改,3 张 SVG 产物同批重跑
- 顺手补并行方案同行漏网计数 51→52 钩子

验证:提交期快钩子对 123 文件实跑全 Passed;check_doc_links --all
rc=0(3038 处、无法判定 0);check_mermaid --all rc=0;YAML 解析 52
钩子不变;skill-size rc=0;py_compile 全过;字节卫生零漂移。
2026-10-30 13:03:12 +08:00

135 lines
17 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.
## 陷阱(实测)
- **`--force` 不是万能钥匙**:它只覆盖四类检查——并发上限、持有者与 `--task` 不符、陈旧锁接管、
清场时强制移除工作树。**台账缺失这类前置/环境错误(exit 2)跟它无关**(实测 `lock release
--force --bench-pass` 报的是同一句话,锁照旧没放)。同理 `abort --force` 也只覆盖「树里有东西」这一条拒绝,
**不会**替你做 E 节的内容保全——它只会删得更快。
- **未跟踪文件在 `git log`、提交门禁与 `done` 的合并里都不留痕**(2026-10-29 回收现场):钩子按 HEAD tree 的
diff-index 取文件,未跟踪文件从没进过任何钩子的视野;`git log` 也照不出「唯一未合并」——它压根没被提交过。
所以「这棵树没有遗留改动」这种结论**只能**由 E 第 2 步的逐文件内容比对给出,`log --oneline -1` + 脏计数都不够。
- **`git status --porcelain` 不带 `-uall` 会折叠未跟踪目录**(E 第 1 步):「全景」少一项、分桶少一个文件,
最后拆掉的是别人唯一的草稿。回收路径上**一律 `-uall`**。
- **`$TW list` 空 ≠ 没有工作树**:`list` 只遍历台账(A 第 1 步),用户说的「那 3 棵 done 掉」很可能在 `$TW list` 里
一个都不存在(实测就是)→ 别照 `$TW list` 的空结果回「没有任务」,先 `git worktree list` 拿全集再逐棵定性(E 节)。
2026-10-29 起 `list` 还会另起一段列出插件树(`.dsh-worktrees/`,F 节)——**"看见"不等于"归 `done` 管"**。
- **「这棵树很久没动」不是回收判据**:实测 `bench-*` 两棵既没台账也没脏文件、mtime 很旧,但真正决定去留的是
`git merge-base --is-ancestor <c> HEAD` = MERGED(提交已进主线)+ 未跟踪层为空;而另一棵 mtime 更新的树反而
全是已在主线的副本。**判据是内容与祖先关系,不是时间戳。**
- **「我记得没建过联接」不是安全判据**:判据只有扫出来的 `REPARSE: none`(E 第 3 步实测 `bench-*` 各挂一个 `.venv`
联接,正是「不是自己建的」那类)。
- **联接不是符号链接**:本仓库 `core.symlinks=false`,git 里的 symlink 会被检出成
「内容是目标路径的文本文件」。共享 `.venv` 只能用 Windows **目录联接**(`mklink /J`,
不需要管理员权限),且联接必须建在版本控制之外。
实测(2026-10-12):用 ctypes 直调 `DeviceIoControl(FSCTL_SET_REPARSE_POINT)`
会被系统拒绝(Win32 4392),`mklink /J` 正常——所以脚本走后者,参数以列表交给
subprocess,含空格的路径(本项目 `DTU Code`)由 subprocess 自动加引号。
但**读属性**用 ctypes 没问题:`GetFileAttributesW` + `FILE_ATTRIBUTE_REPARSE_POINT` 就是 E 第 3 步的预扫手段。
- **联接建失败不中止 `new`**:只告警并打印手工补建命令,避免留下半成品任务空间;
但该任务里跑不了 `.venv\Scripts\python.exe`,`/keil-build` / `ci_local.py` 会找不到解释器。
- **`done` 前主工作树必须干净**:否则未提交改动会被卷进合并(补完基线再重跑 `done` 时最容易忘,见 C 第 6 步)。
- **⚠ 任务树里 `check_doc_links.py --all` 的红多半是 `simulator/SDL2` 目录联接的假红**:全库扫描会吞下那份
「仓库里的第二份仓库副本」(实测任务树 222 个文档 / 35 处问题全在联接下、RC=1,而主树同命令基线全绿)。
判红必须先过滤 `simulator/SDL2` 下的问题行、再与主树**同一条命令**的基线比对;正解是树内按单文件跑、
`--all` 基线合并后在主树跑 → **G 节**。
- **新建的任务树里没有 `ci-artifacts/`**(被 `.gitignore` 忽略 → 不被检出):`> ci-artifacts/x.txt` 与
`fs.writeFileSync("<tree>/ci-artifacts/x", …)` 都会 `ENOENT`;`cd` 进树后再用 `../_wt/x.txt` 落到 `_wt/_wt/` →
`No such file or directory`(两起实测,第二次直接废掉一整轮体检)。落盘前先 `mkdir -p ci-artifacts`,
树内输出一律用树内相对路径 → **G 节**。
- **`done` 报「主工作树不干净」而那份脏改动不是你的**(别的并发会话 / 插件留下的,实测两次都是
`.dsh/skills/serial-boot-test/scripts/run_boot_test.py`):**不许用 stash / 提交 / 删除去清场**(项目级 Never,
你也无从判断它写没写完)→ 走 **C2**:取证(mtime + diff + `hash-object` 存进 ci-artifacts)→ `merge-tree` 预演
→ 主树 `merge --no-ff -F` → 合并前后 blob id 与 `diff --stat` 对账。**「它看着像废弃的」不是丢弃判据**。
- **⚠ `done` 的门禁跑在 `/keil-build` 之前 → 依赖 `.map` 的钩子会误报**(2026-10-12 实测):
`done` 的顺序是「合并 → 重生成 → **门禁(ci_local)** → `/keil-build`」,而
`mem-budget`(登记簿条目「缩堆迁 CCM 后半(暂缓)」)/ `mem-numbers` 是拿**文档快照 vs `.map`** 比对 —— 门禁跑时
`.map` 还是**主工作树合并前**的旧产物,与刚合并进来的新台账**必然不一致** →
误报 `[FAIL] 台账受管区与 .map 不一致`。`/keil-build` 跑完后 `.map` 才是新的。
**处置(2026-10-25 补全)**:按 C 节第 5 步的完整链补——`check_mem_numbers.py --write` →
`ram_baseline.py --write` → `ram_baseline.py --update` → `check_test_coverage.py --update-baseline` →
`check_doc_links.py --all` → 提交 → 重跑 `done`(它会检测到「已合入」直接清场)。
此时主干已含合并提交,**绝不能用提示里的 `reset --hard` 回退**。
- **`done` 的门禁 = `ci_local.py` = `pre-commit run --all-files`(50 条钩子全跑)**(2026-10-26):红了先读
`[SUMMARY] static: pre-commit hooks -- passed=N failed=M skipped=K`,**按钩子 id 定根因**,别一律当「`.map` 滞后」
去跑补基线链。典型例外:`generated-fresh`(生成器幂等性,触发面 `tools/**`、`docs/images/*.svg`、UI 生成文件)——
合进来的分支改了生成器却没重跑产物时,补基线链救不了,处置是在**主工作树重跑那个生成器**、脚本与产物同一笔提交,
再重跑 `done`。另注意 `pre-commit run --all-files` 只跑 pre-commit stage,个别只在 commit-msg / 手工阶段跑的守卫不在此列
(口径见 `scripts/ci_local.py` 头部说明)。
- **⚠ 新建工作树从未构建过 → 体积类门禁必然红**(2026-10-25 实测):`.map` 是每棵树各自的
构建产物、不入版本库,所以 `new` 出来的空间里 `mem-numbers` / `mem-budget` / `ci_local.py`
的体积门禁步都拿不到事实源,报 `[FAIL] 找不到 .map 事实源`(脚本提示已写明原因,不是代码缺陷)。
同理 `done` 的重生成步骤会 `[skip] 未找到 .map,跳过内存快照同步(需先 /keil-build)`。
**处置**:新空间第一次提交前先 `/keil-build` 一次(`build` 锁自动排队,与别的树串行)。与上一条
的区别要分清:上一条是「有 `.map` 但是旧的」,本条是「根本没有 `.map`」,处置动作不同。
- **⚠ 只改注释里的文档路径,也会把「体积同码凭证」刷红**(2026-10-30 实测,`done testdoc`):
`scripts/check_size_gate.py` 的判据是**索引 blob 指纹**(实测 1466 个文件),**不是体积数字**——
那笔全库路径同步顺带改了 20 个 `.c/.h` 的注释,`Program Size` 逐项没变
(Code=246160 / RO-data=29816 / RW-data=2036 / ZI-data=149700),凭证照旧红:
`本次代码(指纹 172c6c78b8fb…,1466 文件)与体积测量(指纹 1ed4267c77f6…,2026-10-30T01:15:01)不同码`。
**处置比上一条那条补绿链短**:合并后 `done` 自己那次 `/keil-build` 已 0 错 0 警,此时只需
`ram_baseline.py --update` 重写凭证(改的是 `scripts/size_attestation.json`;同日同数字时
`ram_baseline.tsv` 连趋势行都不会新增),**别顺手 `--write` 去动内存受管快照**——数字没变就改受管区,
等于给别的在飞分支造冲突面(台账归属口径见 `mem-budget` skill)。
**判据顺序**:先比 `Program Size` 与合并前是否逐项相同 → 相同就只补凭证;不同才走 C 节第 5 步全链。
连带的第二个坑:`done` 对「分支已合入」的路径**在门禁之前就 return**(`cmd_done` 里
`already and ahead == 0` 那条分支直接清场),所以补完凭证重跑 `done` **不会替你复绿**——
落主干后的全量证据得自己跑:`scripts/ci_local.py`(实测 47 runner / 3896 断言全绿)
+ 主树 `check_doc_links.py --all`(实测 215 个文档 / 2993 处章节引用 rc=0)。
- **⚠ 增量构建可能不重链 → `.map` 数字瞬态不一致**(2026-10-12 实测):
`/keil-build` 若判定无需重链,`.map` 保持旧值,此时 `--write` 生成的快照是**瞬态值**
(实测同一份源码两次读到 223,560 与 223,724,差 164 B)。**以主工作树(集成点)
的 `.map` 为准**;改完 RAM 相关代码后确认重链再刷新快照。
- **`ram_baseline.py` 的 help 串里有未转义的 `%`**(2026-10-25 实测:某些直调姿势会渲染 argparse help 并崩)——
查用法读脚本头部 docstring,别靠 `--help`;修脚本本身属 `mem-budget` skill 的范围。
- **合并冲突常落在「生成物」上**:`docs/内存管理/RAM使用分析与优化方案.md`(mem-numbers
快照)在双方都改过时必冲突,且**两边都不能直接选**(各自基于不同基线)——
正确做法是解冲突后用**合并后的实际构建**重新生成,不要挑一侧。
- **并集合并残留**:双方都改过的文件(尤其 `.gitignore`)会被 git 自动并成**并集**,
可能留下指向已删目录的死规则或重复条目。脚本会自动报「双方都改过」的文件清单 +
连续重复行;**权威判据是 base / 两个 tip 三方对比**(`git show <ref>:<file>` 逐项 `grep -F`),
别用 `tail -N` 目测。
- **⚠ 别用 heredoc 打补丁改门禁脚本造探针**(2026-10-27 实测三连败,白烧一整轮):heredoc 里的 python
把门禁脚本改坏——`patched 3` 之后 `PROBE_RC=1`、`SyntaxError: unterminated string literal (detected at
line 882)`,而它**正是被测对象**,改完的数不可信;另一处 `replace('\', '/')`(想把 Windows 反斜杠换成正斜杠)里的反斜杠被多层引号吞掉
→ `SyntaxError (detected at line 16)`。**正解**:探针落成**独立 `.py` 文件**(写文件的工具,UTF-8 无 BOM)
再 `"$PY" probe.py`;非用 heredoc 不可就用单引号定界 `<<'PY'`(防变量展开),且**路径一律 `as_posix()`、
一个反斜杠都不写**——反斜杠字面量要穿「JS 模板串 → bash → shell 引号 → python」四层,必被吞。
E 节那两段扫描脚本同理:**落成 `ci-artifacts/*.py` 再跑**(该目录已被忽略,不会污染第 5 步复验用的脏行数)。
- **取门禁数字:import 门禁模块,别猜输出、别打补丁**(2026-10-27;grep 源码 / 手抄正则 / 解析 stdout 三种姿势
都拿不到可复核的数,换成这句立刻拿到):先读门禁脚本的 `main()`(直读末尾那几十行)看它怎么装配检查器,
把**那几行照抄**进探针——`sys.path.insert(0, 'scripts')` → `import check_ids as ci` →
`ci.load_json(ci.REGISTRY_FILE, 'reg')` + `ci.load_json(ci.ALIASES_FILE, 'alias')` →
`ck = ci.Checker(reg, al, verbose=False)` → `ck.run()` → `print(ck.stats)`。别猜键名:先
`print(sorted(ck.stats))` 列出全量计数项,再按精确键取数(顺带 `git diff` 确认门禁脚本自己**没被改过**)。
- **别把「不可判定」当成一个数**(2026-10-27):`id-check` 有两种口径——检查器自己算的**硬不可判定**实测只有
**2** 处,而门禁红线(`scripts/archive/id_gate/id_registry.json` 的 `undecidable_total_ceiling`)量的是**派生值**
`refs_total - decidable`(实测 8,744 - 7,582 = **1,162**,拆开 = `skip_bare_unmanaged` 834 +
`noise_allowed` 326 + 硬不可判定 2,**三数精确闭合**)。所以**带属主登记处的族(TC-XXX 这类)加多少引用都算
「可判定」、一点不吃这条余量**,真正吃余量的是**裸形字母+数字**字面量;反过来「把可判定引用改写成散文」才是
**稀释比值**的动作,那才是绝对数红线要防的。口径混用会当场给整条道下错指令(本次就下错过、量完立刻撤回)。
- **队友的自查结论要复核,小心它测的是错变量**(2026-10-27):log 道做过受控实验、结论是「我们没推高这个数」——
他们改的是 TC 号、盯的是 `skip_bare_unmanaged`,那个组合当然不动;而 +16 走的是另一条路(新增裸形 「字母 T 累进数字 4」)。
定责用 D 节:逐文件注入 + HEAD/在飞两版同一计数器相减 + 对新增行 grep;闭合后 +16 = 16 处裸形 「字母 T 累进数字 4」,
**推翻**了自查结论。口头自查与 `--note` 都不是证据,**可复核的差分数字才是**。
- **`.workbuddy-ai/` 刻意不共享**:记忆不随任务分叉,也不给联接递归删除的机会。
任务工作树自己的记忆由 `done` / `abort` 在清场前归并回主工作树。⚠ **手工回收(E 节)没有这一步归并**:
拆之前顺手 `ls <tree>/.workbuddy-ai/` 看一眼,有内容就一并备份进 `ci-artifacts/wt-leftover-backup/`。
- **`--lean` 只省磁盘不省时间**:实测全量 smudge 7.25 s / 83 MB,跳过 smudge 7.05 s / 45 MB,
两者工作区都干净。默认走全量(参考图可用),磁盘紧张时才用 `--lean`。
- **锁不绑定进程存活**:`lock acquire` 本身是短命进程,而真正用资源的烧录/构建在别的进程里跑。
所以判「陈旧」只看**时效**(该锁的 TTL)加显式 `--force`;pid 仅作排障记录。
同任务重复 acquire 是幂等的(不会把自己锁死)。
- **TTL 按锁分设**(2026-10-12):`build` = **1h**、`bench` = **6h**(`LOCK_TTL_HOURS`)。
口径是「兜底窗口应与操作真实时长同量级」——编译是秒~分钟级,6h 的兜底太钝;
bench 是会话级(flash→`/serial-boot-test`→探针),才配得上小时级。持有者还可用
`lock acquire ... --ttl <小时>` **自报**更精确的窗口(`keil_build.py` 即按其 `--timeout` 自报)。
- **陈旧锁自动接管**:超过该锁 TTL 未释放即视为陈旧,下次 `acquire` 会接管并提示;
未到 TTL 的活锁需 `--force`。`done` 的收口前置也会**自动释放陈旧锁**(不再硬失败),但
**未到 TTL 的活锁会把 `done` 挡在前置检查**(B 节第 8 步)。
- **`build` 锁不必手工持**(2026-10-12 起):`/keil-build`(`keil_build.py`)**自持自释**——
进入 acquire、`finally` 无条件 release,所以"Agent 忘了 release、锁一直挂到 TTL"这条在构建路径上已消除。
`done` 的门禁自己持锁并给子进程设 `DTU_LOCK_HELD=build` 告知跳过(新旧脚本都兼容)。
**`bench` 锁仍须 Agent 手工持**(会话作用域:跨烧录→`/serial-boot-test`→探针,见 `keil-build` SKILL.md)。
- **⚠ `done --dry-run` 只保护预演段、不保护清场段**(2026-10-29 实测):`done <ID> --dry-run --skip-build --skip-gates` 在「分支已合入主干」这条路上**照样执行清场**——摘 `.venv` / `simulator/SDL2` 联接、删分支、清台账,然后打「完成(分支此前已合入)」。想真·零改动就只跑不带 `--skip-gates` 的 `--dry-run` 并停在预演输出;用完一律 `git worktree list` 复验(判完成以 `git log` 为准,不是以有没有再敲一次 `done` 为准)。另:`--dry-run` 的前置检查同样要抢 `build` 锁——被别的任务占着时它会直接报「独占资源 build 仍被 X 占用,先 release」,此时加 `--skip-build` 或先 release,**别为了预演去抢别人的锁**。
- **⚠ 文件工具「先读后改」按解析后的绝对路径记账,切树后要重新读**(2026-10-30 实测,屏板链路任务树):开新树后先用**相对路径**读过一批文件(读历史记到当时解析到的主树文件上),再进任务树直接 `edit` → 6 条并行编辑全被 `file has not been read — read the file, then retry` 拒。判据:**树内改动一律用树内绝对路径,先 read 再改**——读历史不跨路径复用,工具不做「同名相对路径」宽容。
- **⚠ 中文门禁输出回显乱码 ≠ 路径坏**(2026-10-30 实测):对含中文文件名的文档跑 `check_doc_links.py`,终端回显整段乱码,但 **rc 与判定计数全部正确**。只认 `[SUMMARY]` 行与退出码,别误诊成「文档丢失」;脚本化批量调用走 python subprocess + 显式 `encoding`,别经命令行中转中文名。