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() 逐个复载确认加载的是拆分版。
14 KiB
14 KiB
陷阱(实测)
--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-budgetskill 的范围。- 合并冲突常落在「生成物」上:
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_unmanaged834 +noise_allowed326 + 硬不可判定 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-buildSKILL.md)。