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

17 KiB
Raw Blame History

陷阱(实测)

  • --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,别经命令行中转中文名。