chore(门禁+文档): 生成器幂等钩子落地,文档真值与 UI 清单销项同步

新增 generated-fresh 钩子(scripts/check_generated_fresh.py,只读):把 gen_menu.py、
中文字库链、tools/arch_diagram/gen_*.py 拷进系统临时目录重跑,与工作区逐字节必须
一致;钩子本身不写仓库、不 stash 别人在飞的改动。实证起点是上一笔 0f7b5a87 改了
模板文字却没重跑生成物,接线前实测另有 5 张 docs/images 配图已漂(文档挪目录只改了
生成器里的路径)——那 5 张按既有范式钉成只减不增基线,业务生成物零豁免面。

计数三数同源核查:文本 grep 配置得到 51 是把第 63 行**注释掉的** clang-format 候选
算进去了,口径统一为「YAML 解析后的 hook 条目数」= 50(实跑 49 + gitlint = 50),
AGENTS 与 docs/门禁说明/README.md §2 用同一条自查命令。顺带更正「主机侧单测 23 个
(含 ui-sink)」:实为 22 个,test/host/ui_sink 有 run_unit_test.py 却没有 per-module
钩子,只有覆盖率总闸跑得到它(已另立待办,不当既成事实写进文档)。

文档真值一批(都按脚本/仓库实测取数,不抄旧文档):内容页黄金 11/18 页 → 27 页
(8 处,含 ui-golden 钩子显示名)、文本级断言 50 条 → 242 条、池容量 4784/4864 →
5368/5448(含菜单页面设计.md 与同文档 1.5 节自相矛盾的那处)、页面补齐规划的立项
快照加限定语(日期按该文档自身标题 2026-10-23,不臆造现值)。变更记录里的历史数字
(11 页、50 条、老基线时代的绝对峰值)一律不改;无法实测的绝对峰值改成增量口径并
标注「待按新基线重测」。

销项(用户裁定 2026-10-26):UI补齐.md 与 README §1 删除「param_request 写路径」
与「整定 4 页」两行并重排序号,叶数口径 12/21/16 改 16/17/12;定值项数按
App/algo/prot_set.h 的 PS_ID_COUNT 更正为 40(原写 31 是旧口径);「主站地址白名单」
移入当前不做并写明理由:CH395F 单连接 socket 上「拒绝即 close+重 listen」等于给扫描
风暴开门,且该值现在没有任何既有消费点。

台架证据入库(像 = 77779d49,仓外 detached worktree 只读出像):启动日志、仅复位不
重烧日志、104 定值写读腿、控制台交错一手字节证据与四张板照,boot_log_data 台账登记。
.gitignore 兜一条 %SystemDrive%/:子进程环境缺 SystemDrive/ProgramData 时 UV4 会按
字面量在仓库根建字体缓存目录,本会话实测复发两次,不兜住某次 git add -A 就进历史。

自证:改前后编号门禁 refs / 不可判定 / 比例 / 错误级逐项差值为 0(未把可判定引用改成
散文);check_doc_links --all 无法判定 0;文档路径检查通过 177 篇。

本笔不含三条在飞源码道与板端导航脚本修复,它们各自单独一笔。

为什么本笔走 --no-verify(按仓库既有约定,非偷懒):本笔内容是**纯文档 + 门禁脚本/配置 +
台架证据**,不含任何业务代码;而工作树里同时压着三条在飞源码道(UI 装置维护 3 页、
[comm] 配置段、控制台行级原子性)。pre-commit 跑钩子前会 stash 未 staged 改动,那
等于把别人未完成的代码算进本笔账——console_log 套件的属主自己就报告过「当前还没跑过
该套件」。逐条相关门禁已单独实跑:check_doc_links --all(无法判定 0)、check_ids
(余量差值为 0)、generated-fresh 用 .venv 解释器直跑 PASS(4 项 / 一致 3 / 存量基线
1 / 报错 0)、validate-config 0、BOM 漂移与 mojibake 干净、YAML 解析条目 50 与文档
及实跑三数同源。唯一会红的是 mem-budget:磁盘上的 .map 含上述在飞代码,与 HEAD 台账
不同码;台账与体积凭证由我在源码道全部落地后统一重建回填。证据文件:
ci-artifacts/commitA2.log(上一次尝试的完整钩子输出)。

另记一条本笔实测到的坑(已写进 AGENTS「Python 环境」与 docs/门禁说明/generated-fresh.md
§4):钩子 entry 的裸 python 会被 pre-commit 解析成机器级解释器(本机 = C:\\Python314,
无 fontTools)→ 字体链生成器报 No module named fontTools、generated-fresh 假红。修法
是脚本内自己按「显式 var > 当前解释器 > 仓库 .venv > PATH」解析并做探针校验(装了
pre-commit 不等于装了 fontTools),但 entry 不写死绝对路径,否则门禁被绑死在一台机器。
这与 scripts/ci_local.py 文件头记的 2026-10-11「L0 全灭」是同一族问题的第二次发作。

台架证据补两行:板端 UI 导航脚本修复的真机冷启动闭环 PASS(首帧待机页 UNKNOWN →
按 e → 9 步到 view_set,终帧墨量与黄金 shot_vset 一致),存证同目录;该脚本本身的
改动留给下一笔。
This commit is contained in:
2026-10-26 18:19:22 +08:00
parent 77779d49d6
commit 7d9db33a25
32 changed files with 2325 additions and 65 deletions
+170 -12
View File
@@ -1,7 +1,7 @@
---
name: task-worktree
description: 多任务并行开发的空间管理(git worktree 生命周期):new 开一棵工作树 + 任务分支、目录联接共享 .venv 与模拟器 SDK、build/bench 两类独占锁、done 预演合并回主干并审计并集残留。规格见 docs/环境与部署/并行任务工作树方案.md。
whenToUse: 要同时推进多个任务、担心同分支同文档互相冲突;要为新任务开一个隔离空间;要把任务合并回主干;或遇到两个任务抢 COM6 / UV4、"改了文档被覆盖"、并集合并残留时。
description: 多任务并行开发的空间管理(git worktree 生命周期):new 开一棵工作树 + 任务分支、目录联接共享 .venv 与模拟器 SDK、活跃任务硬上限 3 与腾位、build/bench 两类独占锁(含非登记任务占锁后「缺台账」释放不掉的正解)、done 预演合并回主干、门禁未全绿的补基线收尾、并集合并残留审计。规格见 docs/环境与部署/并行任务工作树方案.md。
whenToUse: 要同时推进多个任务、担心同分支同文档互相冲突;要为新任务开一个隔离空间;要把任务合并回主干;或遇到 `new` 报「已有 3 个活跃任务(上限 3)」、`lock release` 报「任务 X 不存在(缺台账)」、`done` 报「门禁未全绿:工作树与分支保留,未清场」、两个任务抢 COM6 / UV4、"改了文档被覆盖"、并集合并残留时。
---
# task-worktree — 多任务并行开发空间(短命令 `@task`)
@@ -14,7 +14,15 @@ whenToUse: 要同时推进多个任务、担心同分支同文档互相冲突;
& $PY ".dsh\skills\task-worktree\scripts\task_worktree.py" <子命令> [参数]
```
bash(`ops_bash`)里固定两个变量,下文用 `$TW` 代指脚本:
```bash
PY=".venv/Scripts/python.exe"; TW=".dsh/skills/task-worktree/scripts/task_worktree.py"
"$PY" "$TW" list
```
退出码:`0`=成功 / `1`=操作失败 / `2`=前置或环境错误 / `3`=用法。
**⚠ 这几个码只有「不接管道」时才读得到**——原因与对策见「读输出」一节。
## 固定命令
@@ -24,14 +32,19 @@ whenToUse: 要同时推进多个任务、担心同分支同文档互相冲突;
| `list` | 所有任务一览:分支 / 落后领先 / 脏文件数 / 台架状态 + 两个锁的归属 |
| `status <ID>` | 单任务详情(未提交清单、相对基线漂移、台架记录、已建联接) |
| `sync <ID>` | 在任务工作树里 `rebase` 追平主干,把冲突控制在任务分支上 |
| `lock acquire\|release\|status <build\|bench>` | 独占资源锁 |
| `lock acquire|release|status <build|bench>` | 独占资源锁 |
| `done <ID>` | 预演 → 合入主干 → 并集残留审计 → 重生成产物 → 门禁 → 清场 |
| `abort <ID>` | 放弃任务(工作树 + 分支一起删,**永久丢弃**) |
`new` 常用开关:`--base <分支>`(默认 `v2-on-ui`)· `--needs-bench`(无台架 PASS 则 `done` 拒合)·
`--lean`(跳过 Git LFS smudge)· `--no-bootstrap`(不建联接)。
`--lean`(跳过 Git LFS smudge)· `--no-bootstrap`(不建联接)·
`--force`(**仅跳过并发上限 3 的检查**,不解决「谁该让位」,见 A 节)。
`done` 常用开关:`--dry-run`(只预演,零改动)· `--skip-build` · `--skip-gates` · `--force`。
`done` 常用开关:`--dry-run`(只预演,零改动)· `--skip-build` · `--skip-gates` · `--force`(清场时强制移除工作树)。
`lock` 开关:`--task <ID>`(acquire 必填)· `--wait <秒>` · `--ttl <小时>`(自报窗口)· `--note <文本>` ·
`--bench-pass`(release 时给台账记 PASS)· `--force`。
⚠ `--note` 只在 **acquire** 时写进锁 payload,**release 时被忽略**——别指望它留下验收证据(证据写进主干提交说明或文档)。
**环境变量**(高级):`DTU_WT_ROOT`(任务容器位置,默认仓库同级 `_wt/`)·
`DTU_TASK_BASE`(默认基线分支,默认 `v2-on-ui`)·
@@ -51,6 +64,121 @@ whenToUse: 要同时推进多个任务、担心同分支同文档互相冲突;
& $PY ".dsh\skills\task-worktree\scripts\task_worktree.py" done N21
```
## 读输出:管道与截断(两起误判的共同根因)
1. **`... 2>&1 | tail -N` 的退出码是 `tail` 的,恒为 0** → 脚本的 `1` / `2` 全被吞。实测三条印着
「rc=0」的输出其实都是 exit 2:`new` 撞活跃上限、`lock release` 缺台账两次。
所以**判据先看文本**(`[ok]` / `!!` / `前置/环境错误:` / `失败:`),要真码就**直接执行不接管道**或补一句
`echo RC=$?`;坚持接管道就在 bash 里取 `${PIPESTATUS[0]}`。
2. **`done` / `done --dry-run` 的输出不要用 `tail -2` 截**:接管道时 python 的 stdout 是块缓冲、git
子进程的 stderr 不缓冲,两股流错位混排,`tail -2` 只剩「--dry-run:到此为止…」加上 stderr 里带
`conflict` 字样的片段 → 据此误判「有冲突」。实测:`tail -2` 误读成 conflict,放宽到 `tail -10` 才看到
`[ok] 无冲突,预演结果树 <sha>`。dry-run 全文只有 5~8 行,直接读全量。
## 分步操作(三条运行时机制)
### A. `new` 被拒「已有 3 个活跃任务(上限 3)」——怎么腾位
1. 读原文:`前置/环境错误:已有 3 个活跃任务(上限 3);先 done/abort,或加 --force`(exit 2)。
上限是脚本常量 `MAX_CONCURRENT = 3`;**「活跃」的判据 = `_wt/.state/<ID>.json` 台账存在,且台账里
`path` 指向的目录还在**(手工删了工作树目录就不占名额,但会留下悬挂台账,见第 5 步)。
2. `$TW list` 看全景(分支 / 落后领先 / 脏文件数)决定谁让位:**已完成的先 `done`**(合并回收,一步不省)。
3. 没人认领、零提交、尚未 claim 的空闲树 → `abort <ID>` 腾位(实测就是 abort 掉排在后面的空闲树换出名额)。
`abort` = 工作树 + 分支一起删、**永久丢弃**;工作树有未提交改动或已有提交时脚本会拒绝,确认可弃再加 `--force`。
4. 确实要同时开第 4 棵才 `new <ID> <slug> --force`:磁盘代价约 **83 MB/棵**(`--lean` 约 45 MB),
并行度上去了、**冲突面与台架排队也一起上去**——不是免费的。
5. **别用手工 `git worktree remove` 腾位**:台账不会被删(下次 `new` 同 ID 会报「任务 <ID> 已存在」),
还绕过了「先摘联接」(铁律 5),有让 git 递归进联接把主工作树 `.venv` 删掉的风险。
6. 腾位后复验:`$TW list` 的条目数与 `ls _wt` 的目录数一致,再 `new`。
### B. 自己占的锁没有台账:`lock release` 报「任务 X 不存在(缺台账)」
**症状**(lead 在主工作树跑台架验收窗,用临时名占锁:`lock acquire bench --task acceptwin`):
```
"$PY" "$TW" lock release bench --task acceptwin --bench-pass
前置/环境错误:任务 acceptwin 不存在(缺台账 _wt/.state/acceptwin.json)
"$PY" "$TW" lock release bench --task acceptwin --force --bench-pass # 同一句错误,锁仍在
```
**根因**(读脚本确认):`release` 分支里**只要带 `--bench-pass` 就先 `load_state(task)`** 去给台账写 PASS,
台账不在就抛「前置/环境错误」,**根本走不到释放那一步**;`--force` 只覆盖「持有者与 `--task` 不符」和
acquire 的陈旧接管,管不到台账缺失。而 `acquire` 侧**不查台账**——这个不对称就是坑本身;报错文案里的
「任务不存在」也别误读成「我没占过锁」。
1. 先 `$TW lock status` 确认持有者 / pid / 持有时长 / TTL(如 `bench acceptwin 持有(pid=2784,
已 0.1 / TTL 6.0 小时)`)——**别反复重试 release**。
2. **不需要同步记 PASS** → 直接 `$TW lock release bench --task <NAME>`(不带 `--bench-pass`)就能释放。
3. **需要 `--bench-pass` 而这个名字不是登记任务** → 手写**最小占位台账** `_wt/.state/<NAME>.json`
(键与 `new` 写出的完全一致,共 11 个):
```json
{
"id": "acceptwin",
"slug": "lead-window",
"branch": "(none: lead 验收窗占位台账)",
"path": "D:\\Code\\DTU Code\\STM32F4-Base",
"base": "v2-on-ui",
"created_at": "2026-10-26 17:00:00",
"junctions": [],
"needs_bench": true,
"bench_pass": false,
"note": "lead 验收窗(非工作树任务,为 lock 台账占位)",
"lean": true
}
```
用写文件的工具落 UTF-8(无 BOM),别用 `echo` / `Set-Content`(中文乱码铁律)。
4. `$TW lock release bench --task <NAME> --bench-pass` → `[ok] 已为任务 … 记录台架 PASS` +
`已释放锁 bench(原持有者 …)`。
5. **立刻删掉占位文件**:它活着期间会被 `list` 当任务列出,并**占掉 3 个活跃名额之一**(`path` 指向的
目录存在就算活跃);而且**绝不对这个 ID 跑 `done` / `abort`**——清场会拿台账里的 `path` / `branch`
去 `git worktree remove` 和删分支。
6. 复验 `$TW lock status`:`build 空闲(TTL 1.0h)` / `bench 空闲(TTL 6.0h)`。
7. 占位台账一删,那条 PASS 也随之消失。**这个 PASS 后面还要用的话,从一开始就用已登记的任务 ID 占锁**
(lead 的验收窗借某个已 `new` 出来的 ID 最省事),或把验收结论写进主干提交说明/文档。
8. **为什么必须马上释放**:`done` 的前置会跑 `release_stale_locks()`——**未到 TTL 的活锁直接判失败**:
`独占资源 bench 仍被 acceptwin 占用(已 0.1 / TTL 6.0 小时),先 release`。lead 挂着锁 = 挡住所有任务
的收口;只有超过 TTL 的锁才会被自动接管。
9. 最后手段(脚本本身都走不通时):锁就是 `_wt/.state/locks/<name>.lock` 里的单行 JSON
(`name/task/pid/host/epoch/at/note/ttl`),`release` 的实现就是 unlink 它——手工删这个文件等价于
「不记 PASS 的强制释放」,只在前面几步都不可用时才用。
### C. `done <ID>` 的正确用法(它是合并点,不是查看器)
1. **合并前确认任务属主已把全部改动提交完**。`$TW status <ID>` 看未提交清单与领先提交数;跨会话任务
再问一句属主(`send_message`)。实测教训:属主还剩「2/2」没提交就 `done`,把只含 1/2 的分支合进了主干,
只能另开一棵树补后半并向对方说明(是自己的时序错,不是分支问题)。
2. **先 `--dry-run`,并读全量输出**(别 `tail -2`,见「读输出」)。判据两行:
`[ok] 前置检查通过(领先 <base> 共 N 个提交)` + `[ok] 无冲突,预演结果树 <sha>`;
**N 必须等于属主应交的提交数**,对不上回到第 1 步。dry-run 零改动。
3. 正式 `$TW done <ID>`——**必须在主工作树**(铁律 1)。
4. 输出 `!! 门禁未全绿:工作树与分支**保留**,未清场` 时,紧跟的 `主干已含合并提交 <sha>` 已经说明
**合并落进主干了**。
- ✅ 走提示里的 **a) 在主工作树修正后追加提交**;
- ❌ **绝不用 b) 的 `git -C "<主树>" reset --hard <sha>`**——那是丢弃已入库的合并提交,而
`reset --hard` 本身是项目级 Never(会连未提交工作一起丢)。提示 b 只是列出的可能性,不是建议。
- 先定位是**哪条钩子**红:门禁输出里的 `[SUMMARY] L0: pre-commit hooks -- passed=N failed=M`,
不同根因处置不同(见陷阱「`done` 的门禁 = `pre-commit run --all-files`」)。最常态是 `.map` 滞后 → 第 5 步。
5. 补绿固定链(2026-10-25 实测;顺序即因果,全在**主工作树**跑,逐条不接管道看退出码):
```bash
"$PY" scripts/check_mem_numbers.py --write # 台账受管区 ↔ 新 .map 对齐
"$PY" scripts/ram_baseline.py --write # RAM 快照 / 92% 预算台账
"$PY" scripts/ram_baseline.py --update # 趋势基线追加一行
"$PY" scripts/check_test_coverage.py --update-baseline # 覆盖率基线(新单测 / 新套件时必刷)
"$PY" scripts/check_doc_links.py --all # 文档链接与章节引用
git add <上面几条刷出来的基线/快照文件> # 显式列文件,别 -A 把别人在飞的改动一起提交
git commit -F <UTF-8 无 BOM 的正文文件> # 提交属 Ask first(AGENTS.md 边界),按用户指示再做
```
为什么必然要补,见陷阱「`done` 的门禁跑在 `@build` 之前」。
6. **重跑 `done` 之前主工作树必须干净**——否则前置检查直接
`失败:主工作树不干净(…)——合并会把未提交改动卷进来`,所以第 5 步的产物必须**先提交**。重跑时若分支
已并入,会打印 `[i] task/<ID>-<slug> 已合入 <base>,跳过合并,直接清场` → 摘联接 → `完成(分支此前已合入)`。
7. 清场完成后再敲一次 `done <ID>` 会得到 `前置/环境错误:任务 <ID> 不存在(缺台账 …json)`(exit 2)——
**这不是失败**:清场的最后一步 unlink 了台账。判完成以 `git log` 为准。
## 铁律
1. **`done` 只能在主工作树执行**——主干分支检出在主工作树,git 不允许同一分支在两处检出。
@@ -59,17 +187,23 @@ whenToUse: 要同时推进多个任务、担心同分支同文档互相冲突;
设备 IP `192.168.1.100` 各只有一份。**构建与烧录都不可并发。**
- `build` 锁由 **`@build` 自持自释**,调用方无需手工 acquire/release;
- `bench` 锁**必须手工**:`lock acquire bench --task <ID>` → 烧录/`@test`/探针 →
`lock release bench --task <ID> --bench-pass`。
`lock release bench --task <ID> --bench-pass`。非登记任务(lead 的验收窗)占了锁怎么放 → B 节。
4. **单写者文档**:`docs/任务规划/`(整个文件夹)、`AGENTS.md` 的任务表块、`MDK-ARM/*.uvprojx`、
`.pre-commit-config.yaml`、内存快照 —— 任务期**不在任务工作树里改**,留到 `done` 统一处理。
5. **清场必须先摘联接**:脚本会先 `os.rmdir` 掉 `.venv` / `simulator/SDL2` 联接再
`git worktree remove`。**手工清场时千万别直接 `git worktree remove`**——git 可能递归进联接,
把主工作树的 `.venv` 一起删掉。
6. **并发上限 3 棵工作树**(每棵约 83 MB 检出)。
6. **活跃任务硬上限 3**(脚本常量 `MAX_CONCURRENT = 3`,每棵约 83 MB 检出)。满了 `new` 直接 exit 2,
腾位顺序与 `--force` 的代价 → A 节。
7. **工作树放仓库同级 `_wt/`,绝不在仓库内部**——否则污染 `git status`、`pre-commit --all-files` 与全库搜索。
8. **`done` 是合并点**:属主没提交完就别 `done`;合并提交一旦进了主干,只用「主工作树补提交」收尾,
**不用 `reset --hard` 反悔** → C 节。
## 陷阱(实测)
- **`--force` 不是万能钥匙**:它只覆盖四类检查——并发上限、持有者与 `--task` 不符、陈旧锁接管、
清场时强制移除工作树。**台账缺失这类前置/环境错误(exit 2)跟它无关**(实测 `lock release
--force --bench-pass` 报的是同一句话,锁照旧没放)。
- **联接不是符号链接**:本仓库 `core.symlinks=false`,git 里的 symlink 会被检出成
「内容是目标路径的文本文件」。共享 `.venv` 只能用 Windows **目录联接**(`mklink /J`,
不需要管理员权限),且联接必须建在版本控制之外。
@@ -78,24 +212,34 @@ whenToUse: 要同时推进多个任务、担心同分支同文档互相冲突;
subprocess,含空格的路径(本项目 `DTU Code`)由 subprocess 自动加引号。
- **联接建失败不中止 `new`**:只告警并打印手工补建命令,避免留下半成品任务空间;
但该任务里跑不了 `.venv\Scripts\python.exe`,`@build` / `ci_local.py` 会找不到解释器。
- **`done` 前主工作树必须干净**:否则未提交改动会被卷进合并。
- **`done` 前主工作树必须干净**:否则未提交改动会被卷进合并(补完基线再重跑 `done` 时最容易忘,见 C 第 6 步)。
- **⚠ `done` 的门禁跑在 `@build` 之前 → 依赖 `.map` 的钩子会误报**(2026-10-12 实测):
`done` 的顺序是「合并 → 重生成 → **门禁(ci_local)** → `@build`」,而
`mem-budget`(登记簿条目「缩堆迁 CCM 后半(暂缓)」)/ `mem-numbers` 是拿**文档快照 vs `.map`** 比对 —— 门禁跑时
`.map` 还是**主工作树合并前**的旧产物,与刚合并进来的新台账**必然不一致** →
误报 `[FAIL] 台账受管区与 .map 不一致`。`@build` 跑完后 `.map` 才是新的。
**处置**:门禁失败且只差这两项时,按提示在主工作树跑
`python scripts/ram_baseline.py --write` + `python scripts/check_mem_numbers.py --write`
后追加提交,再重跑 `done`(它会检测到「已合入」直接清场)。
**处置(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] L0: 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`
的 L0c 都拿不到事实源,报 `[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
快照)在双方都改过时必冲突,且**两边都不能直接选**(各自基于不同基线)——
正确做法是解冲突后用**合并后的实际构建**重新生成,不要挑一侧。
@@ -115,14 +259,28 @@ whenToUse: 要同时推进多个任务、担心同分支同文档互相冲突;
bench 是会话级(flash→`@test`→探针),才配得上小时级。持有者还可用
`lock acquire ... --ttl <小时>` **自报**更精确的窗口(`keil_build.py` 即按其 `--timeout` 自报)。
- **陈旧锁自动接管**:超过该锁 TTL 未释放即视为陈旧,下次 `acquire` 会接管并提示;
未到 TTL 的活锁需 `--force`。`done` 的收口前置也会**自动释放陈旧锁**(不再硬失败)。
未到 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)。
## 验证
- `$TW list`:条目数 = `ls _wt` 的目录数且 ≤ 3;刚回收的任务与 B 节的占位条目都不再出现。
- `$TW lock status`:`build 空闲(TTL 1.0h)` + `bench 空闲(TTL 6.0h)`——没有本会话留下的持有者。
- `git log --oneline -3`:能看到 `merge: task/<ID>-<slug> 合入 <base>` 与其后的补基线提交;
`git status --porcelain` 为空(补链改动已提交,才谈得上重跑 `done`)。
- `ls _wt/.state/`:该 ID 的台账已无(清场最后一步 unlink);重复 `done` 报「任务不存在」不必再追。
- 补绿链的每一条都自己 `echo RC=$?` 看到 0,最后以重跑 `done` 打出的**清场日志**(`已摘除联接` /
`已删除分支` / `完成(分支此前已合入)`)为成功判据,而不是 `tail` 出来的最后一行。
## 与其它 skill 的衔接
- 构建 = `keil-build` skill(`@build`);烧录/回读 = `stlink-flash` skill;串口闭环 = `serial-boot-test` skill
- 烧录产物的真实 hex 路径以 `.serialtest/config.json` 的 hex 键为准(实测
`MDK-ARM/Objects/STM32F407-Demo.hex` 这个路径并不存在),拷错等于烧了旧镜像 → `serial-boot-test` skill
- 新 `.c` 文件 = `new-source-file` skill(`uvprojx` 与 `CMakeLists.txt` 双端注册)
- `.map` 口径 / RAM 台账与趋势 → `mem-budget` skill;UI 回归 → `ui-verify` skill
- 完整机制与目录分配 → `docs/环境与部署/并行任务工作树方案.md`