f08b12a46c
按「主文留头量、深规程外置」重写本仓技能,全部是逐字搬移:
搬移前断言原文每条非空行必须出现在新主文或 reference 文件里,
0 丢失才落盘;步骤编号一律不重排(正文有「第 12 步」交叉引用);
每个切点保留原小标题 + 索引(加粗引导句与步骤标题)+ 指针行。
主文变化(上限口径 = 插件写侧 65536 字节,见上一笔 skill-size 钩子):
- id-family-semantic-migration 33392 -> 10968(17%),reference 3 篇
- stale-doc-count-sweep 19811 -> 6945(11%),reference 2 篇
- mem-budget 19963 -> 13042(19%),reference 3 篇
- keil-build 12917 -> 8879(14%),reference 2 篇
- task-worktree 72601 -> 28139(42%),reference 8 篇
task-worktree 本笔同时收录另一会话已写完的 C2/D/G 三节正文——那
三处手改与我的拆分落在同一份文件改动里,不可分离;其内容同样已
随 reference/ 外置,主文只剩标题、索引与指针。
外置语料合计 122211 字节 / 30 篇:围栏数逐篇为偶,无 BOM/CR/U+FFFD。
draw-diagram 与 serial-boot-test 脚本是另一会话的在飞改动,本笔未含。
skill-size 钩子在本笔首次真跑(前一笔无 .dsh/skills 文件可触发)。
gate: pre-commit 全绿(doc-links、id-check-new、check-yaml、
skill-size、check-added-large-files);30 篇 reference 手工字节
体检 clean;skill() 逐个复载确认加载的是拆分版。
118 lines
14 KiB
Markdown
118 lines
14 KiB
Markdown
## 陷阱(实测)
|
||
|
||
- **`--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`,`@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` 的门禁跑在 `@build` 之前 → 依赖 `.map` 的钩子会误报**(2026-10-12 实测):
|
||
`done` 的顺序是「合并 → 重生成 → **门禁(ci_local)** → `@build`」,而
|
||
`mem-budget`(登记簿条目「缩堆迁 CCM 后半(暂缓)」)/ `mem-numbers` 是拿**文档快照 vs `.map`** 比对 —— 门禁跑时
|
||
`.map` 还是**主工作树合并前**的旧产物,与刚合并进来的新台账**必然不一致** →
|
||
误报 `[FAIL] 台账受管区与 .map 不一致`。`@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,跳过内存快照同步(需先 @build)`。
|
||
**处置**:新空间第一次提交前先 `@build` 一次(`build` 锁自动排队,与别的树串行)。与上一条
|
||
的区别要分清:上一条是「有 `.map` 但是旧的」,本条是「根本没有 `.map`」,处置动作不同。
|
||
- **⚠ 增量构建可能不重链 → `.map` 数字瞬态不一致**(2026-10-12 实测):
|
||
`@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→`@test`→探针),才配得上小时级。持有者还可用
|
||
`lock acquire ... --ttl <小时>` **自报**更精确的窗口(`keil_build.py` 即按其 `--timeout` 自报)。
|
||
- **陈旧锁自动接管**:超过该锁 TTL 未释放即视为陈旧,下次 `acquire` 会接管并提示;
|
||
未到 TTL 的活锁需 `--force`。`done` 的收口前置也会**自动释放陈旧锁**(不再硬失败),但
|
||
**未到 TTL 的活锁会把 `done` 挡在前置检查**(B 节第 8 步)。
|
||
- **`build` 锁不必手工持**(2026-10-12 起):`@build`(`keil_build.py`)**自持自释**——
|
||
进入 acquire、`finally` 无条件 release,所以"Agent 忘了 release、锁一直挂到 TTL"这条在构建路径上已消除。
|
||
`done` 的门禁自己持锁并给子进程设 `DTU_LOCK_HELD=build` 告知跳过(新旧脚本都兼容)。
|
||
**`bench` 锁仍须 Agent 手工持**(会话作用域:跨烧录→`@test`→探针,见 `keil-build` SKILL.md)。
|