4a4ad6bc5f
现状口径:/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 全过;字节卫生零漂移。
135 lines
17 KiB
Markdown
135 lines
17 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`,`/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`,别经命令行中转中文名。
|