新增 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 一致),存证同目录;该脚本本身的
改动留给下一笔。
23 KiB
name, description, whenToUse
| name | description | whenToUse |
|---|---|---|
| task-worktree | 多任务并行开发的空间管理(git worktree 生命周期):new 开一棵工作树 + 任务分支、目录联接共享 .venv 与模拟器 SDK、活跃任务硬上限 3 与腾位、build/bench 两类独占锁(含非登记任务占锁后「缺台账」释放不掉的正解)、done 预演合并回主干、门禁未全绿的补基线收尾、并集合并残留审计。规格见 docs/环境与部署/并行任务工作树方案.md。 | 要同时推进多个任务、担心同分支同文档互相冲突;要为新任务开一个隔离空间;要把任务合并回主干;或遇到 `new` 报「已有 3 个活跃任务(上限 3)」、`lock release` 报「任务 X 不存在(缺台账)」、`done` 报「门禁未全绿:工作树与分支保留,未清场」、两个任务抢 COM6 / UV4、"改了文档被覆盖"、并集合并残留时。 |
task-worktree — 多任务并行开发空间(短命令 @task)
执行体 = 本 skill 自带脚本 .dsh/skills/task-worktree/scripts/task_worktree.py
(纯标准库,任意 python3;拷整个 skill 目录到别的项目即可用)。
# cwd = 仓库根(或任意子目录)
& $PY ".dsh\skills\task-worktree\scripts\task_worktree.py" <子命令> [参数]
bash(ops_bash)里固定两个变量,下文用 $TW 代指脚本:
PY=".venv/Scripts/python.exe"; TW=".dsh/skills/task-worktree/scripts/task_worktree.py"
"$PY" "$TW" list
退出码:0=成功 / 1=操作失败 / 2=前置或环境错误 / 3=用法。
⚠ 这几个码只有「不接管道」时才读得到——原因与对策见「读输出」一节。
固定命令
| 命令 | 作用 |
|---|---|
new <ID> <slug> |
建分支 task/<ID>-<slug> + 工作树 + 共享联接 + 台账;打印新空间路径 |
list |
所有任务一览:分支 / 落后领先 / 脏文件数 / 台架状态 + 两个锁的归属 |
status <ID> |
单任务详情(未提交清单、相对基线漂移、台架记录、已建联接) |
sync <ID> |
在任务工作树里 rebase 追平主干,把冲突控制在任务分支上 |
| `lock acquire | release |
done <ID> |
预演 → 合入主干 → 并集残留审计 → 重生成产物 → 门禁 → 清场 |
abort <ID> |
放弃任务(工作树 + 分支一起删,永久丢弃) |
new 常用开关:--base <分支>(默认 v2-on-ui)· --needs-bench(无台架 PASS 则 done 拒合)·
--lean(跳过 Git LFS smudge)· --no-bootstrap(不建联接)·
--force(仅跳过并发上限 3 的检查,不解决「谁该让位」,见 A 节)。
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)·
DTU_INTEGRATION_WORKTREE(集成点,默认主工作树——用于隔离演练/测试)。
典型流程
& $PY ".dsh\skills\task-worktree\scripts\task_worktree.py" new N21 dhara-isfree --needs-bench
# → 把新会话的工作目录切到打印出来的 _wt/N21-dhara-isfree/,在那里干活
& $PY ".dsh\skills\task-worktree\scripts\task_worktree.py" lock acquire bench --task N21
# 烧录 / @test / 板级套件……
& $PY ".dsh\skills\task-worktree\scripts\task_worktree.py" lock release bench --task N21 --bench-pass
# 回到主工作树执行回收
& $PY ".dsh\skills\task-worktree\scripts\task_worktree.py" done N21
读输出:管道与截断(两起误判的共同根因)
... 2>&1 | tail -N的退出码是tail的,恒为 0 → 脚本的1/2全被吞。实测三条印着 「rc=0」的输出其实都是 exit 2:new撞活跃上限、lock release缺台账两次。 所以判据先看文本([ok]/!!/前置/环境错误:/失败:),要真码就直接执行不接管道或补一句echo RC=$?;坚持接管道就在 bash 里取${PIPESTATUS[0]}。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)」——怎么腾位
- 读原文:
前置/环境错误:已有 3 个活跃任务(上限 3);先 done/abort,或加 --force(exit 2)。 上限是脚本常量MAX_CONCURRENT = 3;「活跃」的判据 =_wt/.state/<ID>.json台账存在,且台账里path指向的目录还在(手工删了工作树目录就不占名额,但会留下悬挂台账,见第 5 步)。 $TW list看全景(分支 / 落后领先 / 脏文件数)决定谁让位:已完成的先done(合并回收,一步不省)。- 没人认领、零提交、尚未 claim 的空闲树 →
abort <ID>腾位(实测就是 abort 掉排在后面的空闲树换出名额)。abort= 工作树 + 分支一起删、永久丢弃;工作树有未提交改动或已有提交时脚本会拒绝,确认可弃再加--force。 - 确实要同时开第 4 棵才
new <ID> <slug> --force:磁盘代价约 83 MB/棵(--lean约 45 MB), 并行度上去了、冲突面与台架排队也一起上去——不是免费的。 - 别用手工
git worktree remove腾位:台账不会被删(下次new同 ID 会报「任务 已存在」), 还绕过了「先摘联接」(铁律 5),有让 git 递归进联接把主工作树.venv删掉的风险。 - 腾位后复验:
$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 侧不查台账——这个不对称就是坑本身;报错文案里的
「任务不存在」也别误读成「我没占过锁」。
- 先
$TW lock status确认持有者 / pid / 持有时长 / TTL(如bench acceptwin 持有(pid=2784, 已 0.1 / TTL 6.0 小时))——别反复重试 release。 - 不需要同步记 PASS → 直接
$TW lock release bench --task <NAME>(不带--bench-pass)就能释放。 - 需要
--bench-pass而这个名字不是登记任务 → 手写最小占位台账_wt/.state/<NAME>.json(键与new写出的完全一致,共 11 个):
{
"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> 的正确用法(它是合并点,不是查看器)
- 合并前确认任务属主已把全部改动提交完。
$TW status <ID>看未提交清单与领先提交数;跨会话任务 再问一句属主(send_message)。实测教训:属主还剩「2/2」没提交就done,把只含 1/2 的分支合进了主干, 只能另开一棵树补后半并向对方说明(是自己的时序错,不是分支问题)。 - 先
--dry-run,并读全量输出(别tail -2,见「读输出」)。判据两行:[ok] 前置检查通过(领先 <base> 共 N 个提交)+[ok] 无冲突,预演结果树 <sha>; N 必须等于属主应交的提交数,对不上回到第 1 步。dry-run 零改动。 - 正式
$TW done <ID>——必须在主工作树(铁律 1)。 - 输出
!! 门禁未全绿:工作树与分支**保留**,未清场时,紧跟的主干已含合并提交 <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 步。
- 补绿固定链(2026-10-25 实测;顺序即因果,全在主工作树跑,逐条不接管道看退出码):
"$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 为准。
铁律
done只能在主工作树执行——主干分支检出在主工作树,git 不允许同一分支在两处检出。done不 push。它结束于本地合入 + 门禁全绿;推送需用户明确许可(项目既有纪律)。build与bench都必须串行:UV4 不能两个实例同时-b;串口 COM6、ST-LINK、 设备 IP192.168.1.100各只有一份。构建与烧录都不可并发。build锁由@build自持自释,调用方无需手工 acquire/release;bench锁必须手工:lock acquire bench --task <ID>→ 烧录/@test/探针 →lock release bench --task <ID> --bench-pass。非登记任务(lead 的验收窗)占了锁怎么放 → B 节。
- 单写者文档:
docs/任务规划/(整个文件夹)、AGENTS.md的任务表块、MDK-ARM/*.uvprojx、.pre-commit-config.yaml、内存快照 —— 任务期不在任务工作树里改,留到done统一处理。 - 清场必须先摘联接:脚本会先
os.rmdir掉.venv/simulator/SDL2联接再git worktree remove。手工清场时千万别直接git worktree remove——git 可能递归进联接, 把主工作树的.venv一起删掉。 - 活跃任务硬上限 3(脚本常量
MAX_CONCURRENT = 3,每棵约 83 MB 检出)。满了new直接 exit 2, 腾位顺序与--force的代价 → A 节。 - 工作树放仓库同级
_wt/,绝不在仓库内部——否则污染git status、pre-commit --all-files与全库搜索。 done是合并点:属主没提交完就别done;合并提交一旦进了主干,只用「主工作树补提交」收尾, 不用reset --hard反悔 → C 节。
陷阱(实测)
--force不是万能钥匙:它只覆盖四类检查——并发上限、持有者与--task不符、陈旧锁接管、 清场时强制移除工作树。台账缺失这类前置/环境错误(exit 2)跟它无关(实测lock release --force --bench-pass报的是同一句话,锁照旧没放)。- 联接不是符号链接:本仓库
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 自动加引号。 - 联接建失败不中止
new:只告警并打印手工补建命令,避免留下半成品任务空间; 但该任务里跑不了.venv\Scripts\python.exe,@build/ci_local.py会找不到解释器。 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才是新的。 处置(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-budgetskill 的范围。- 合并冲突常落在「生成物」上:
docs/内存管理/RAM使用分析与优化方案.md(mem-numbers 快照)在双方都改过时必冲突,且两边都不能直接选(各自基于不同基线)—— 正确做法是解冲突后用合并后的实际构建重新生成,不要挑一侧。 - 并集合并残留:双方都改过的文件(尤其
.gitignore)会被 git 自动并成并集, 可能留下指向已删目录的死规则或重复条目。脚本会自动报「双方都改过」的文件清单 + 连续重复行;权威判据是 base / 两个 tip 三方对比(git show <ref>:<file>逐项grep -F), 别用tail -N目测。 .workbuddy-ai/刻意不共享:记忆不随任务分叉,也不给联接递归删除的机会。 任务工作树自己的记忆由done/abort在清场前归并回主工作树。--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)。
验证
$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-buildskill(@build);烧录/回读 =stlink-flashskill;串口闭环 =serial-boot-testskill - 烧录产物的真实 hex 路径以
.serialtest/config.json的 hex 键为准(实测MDK-ARM/Objects/STM32F407-Demo.hex这个路径并不存在),拷错等于烧了旧镜像 →serial-boot-testskill - 新
.c文件 =new-source-fileskill(uvprojx与CMakeLists.txt双端注册) .map口径 / RAM 台账与趋势 →mem-budgetskill;UI 回归 →ui-verifyskill- 完整机制与目录分配 →
docs/环境与部署/并行任务工作树方案.md