唯一事实源是脚本常量 MAX_CONCURRENT(task_worktree.py:30),报错文案与 new 的插件树提示都插值它; 其余全是引用面同步:SKILL.md 12 行(含 frontmatter 的 description 与 whenToUse——它会进技能目录)、 方案文档表格行 + 依据段重写、启动日志原子性复验流程 1 行去掉数字。 依据段按今天的实测重写,不再拿磁盘当理由:一棵纯净检出 84.0 MiB / 2857 个受控文件(docs 独占 46 MiB),.venv 与 simulator/SDL2 走目录联接共享、不随树复制,D 盘余 1.4 TB → 8 棵全开最坏约 3.2 GB(每棵再叠一次构建产物:Keil 中间件 289 MB + 模拟器 35 MB)。真正随并行数上涨的是排队: build 锁 TTL 1.0h、bench 锁 TTL 6.0h 分别串行化 @build 与 @test,done 的合并也必须串行——8 棵是 8 个空间,仍是一套 Keil 授权 + 一块板。 扫描按全形态跑(上限 3 / 最多 3 / 第 4 棵 / MAX_CONCURRENT = 3 / 硬上限 3 / 名额),整行打印后 另抓到两处只按数字搜不到的形态:whenToUse 里的「白占 3 个名额之一」、「确实要同时开第 4 棵」 (上限变了它必须跟着变第 9 棵,否则自相矛盾)。 冻结不改并写明类别:带日期戳的历史测量(2026-10-28 一次回收 3 棵、各 89 MB)、用户原话引语、 列表与步骤序号。「83 MB」是当时 du 口径、与本次内容字节和 84.0 MiB 同数不同义 → 只补口径注并 指回方案 §4,不合并成同一个数;方案 §2 的「对象库 234 MB」保留——今天的 294 MB 含待两周宽限期 过期的不可达对象,不把临时态钉进文档。 本轮我自己写错并已改掉:新段落引用了不存在的子命令 lock list,真命令是 lock status(全仓零残留)。 验证:上限生效用 %TEMP% 隔离小仓实测(脚本按同深度拷贝,因 REPO_ROOT 由 __file__ 反推)——前 8 棵 全成功,第 9 棵 exit 2 且文案为「已有 8 个活跃任务(上限 8)」,--force 照旧放行,list 9 行;真仓 零副作用(worktree list 仍 1 条、同级 _wt 无新建目录、无 t0* 残留)。门禁:doc-links --all --sections-strict RC=0(2601 处、无法判定 0)、编号门禁 [OK]、py_compile RC=0、四文件无 BOM、 纯 LF、无 U+FFFD、无行尾空格。改动不落体积门禁指纹面,不需要 @build。
57 KiB
name, description, whenToUse
| name | description | whenToUse |
|---|---|---|
| task-worktree | 多任务并行开发的空间管理(git worktree 生命周期):new 开一棵工作树 + 任务分支、目录联接共享 .venv 与模拟器 SDK、活跃任务硬上限 8 与腾位、build/bench 两类独占锁(含非登记任务占锁后「缺台账」释放不掉的正解)、done 预演合并回主干、门禁未全绿的补基线收尾、并集合并残留审计、**非登记工作树(detached HEAD / 无台账 / 树内有在飞或未跟踪内容)的安全回收**(逐文件内容分桶 + 联接/reparse 预扫 + 唯一残留先备份再 --force + 悬挂台账清理复验),以及**把仓外 detached worktree 当净室测量仪**——门禁指标异常增长时一次只注入一份在飞文件、与基线差分做逐文件归因;**以及与 DSH worktree 插件(`/worktree`、模型工具 `worktree_create`,树在仓内 .dsh-worktrees/)的桥接**——`plugin adopt` 补共享联接、锁归属 `pw:<name>`、回收仍归 /worktree。规格见 docs/环境与部署/并行任务工作树方案.md。 | 要同时推进多个任务、担心同分支同文档互相冲突;要为新任务开一个隔离空间;要把任务合并回主干;或遇到 `new` 报「已有 8 个活跃任务(上限 8)」、`lock release` 报「任务 X 不存在(缺台账)」、`done` 报「门禁未全绿:工作树与分支保留,未清场」、两个任务抢 COM6 / UV4、"改了文档被覆盖"、并集合并残留时;或**门禁指标异常增长(如 id-check 的不可判定数从 1,162 涨到 1,178、+16)却查不出是哪个文件 / 哪条任务道造成的**、主工作树压着多条在飞改动而不许 stash / 提交 / 改门禁脚本、要按 HEAD 基线只读出像验收时;或**要回收 `@task list` 里根本看不到的工作树**(`git worktree list` 有它、台账没有 / detached HEAD / 树里疑似留着别的会话的在飞或未跟踪内容)、或**要清台账里 branch / path 都已不存在的悬挂条目**(`@task list` 输出发怪、`new` 报「任务已存在」或白占 8 个名额之一);或**这棵隔离空间是 DSH worktree 插件开的**(会话选了「Worktree模式」/ `/worktree create`,树在仓库内 `.dsh-worktrees/`)——进去发现 `.venv` 不在、`@build` 与门禁全部起跑失败,或想看清谁在占 UV4 / 台架 → F 节。 |
task-worktree — 多任务并行开发空间(短命令 @task)
执行体 = 本 skill 自带脚本 .dsh/skills/task-worktree/scripts/task_worktree.py
(纯标准库,任意 python3;拷整个 skill 目录到别的项目即可用)。
@T3@powershell
cwd = 仓库根(或任意子目录)
& $PY ".dsh\skills\task-worktree\scripts\task_worktree.py" <子命令> [参数] @T3@
bash(ops_bash)里固定两个变量,下文用 $TW 代指脚本:
@T3@bash PY=".venv/Scripts/python.exe"; TW=".dsh/skills/task-worktree/scripts/task_worktree.py" "$PY" "$TW" list @T3@
退出码:0=成功 / 1=操作失败 / 2=前置或环境错误 / 3=用法。
⚠ 这几个码只有「不接管道」时才读得到——原因与对策见「读输出」一节。
固定命令
| 命令 | 作用 |
|---|---|
new <ID> <slug> |
建分支 task/<ID>-<slug> + 工作树 + 共享联接 + 台账;打印新空间路径 |
list |
所有任务一览:分支 / 落后领先 / 脏文件数 / 台架状态 + 两个锁的归属 |
status <ID> |
单任务详情(未提交清单、相对基线漂移、台架记录、已建联接) |
sync <ID> |
在任务工作树里 rebase 追平主干,把冲突控制在任务分支上 |
| `lock acquire | release |
| `plugin list | adopt |
done <ID> |
预演 → 合入主干 → 并集残留审计 → 重生成产物 → 门禁 → 清场 |
abort <ID> |
放弃任务(工作树 + 分支一起删,永久丢弃) |
new 常用开关:--base <分支>(默认 v2-on-ui)· --needs-bench(无台架 PASS 则 done 拒合)·
--lean(跳过 Git LFS smudge)· --no-bootstrap(不建联接)·
--force(仅跳过并发上限 8 的检查,不解决「谁该让位」,见 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(集成点,默认主工作树——用于隔离演练/测试)。
典型流程
@T3@powershell & $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 @T3@
读输出:管道与截断(两起误判的共同根因)
... 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 被拒「已有 8 个活跃任务(上限 8)」——怎么腾位
- 读原文:
前置/环境错误:已有 8 个活跃任务(上限 8);先 done/abort,或加 --force(exit 2)。 上限是脚本常量MAX_CONCURRENT = 8;「活跃」的判据 =_wt/.state/<ID>.json台账存在,且台账里path指向的目录还在(手工删了工作树目录就不占名额,但会留下悬挂台账,见第 5 步与 E 第 6 步)。 $TW list看全景(分支 / 落后领先 / 脏文件数)决定谁让位:已完成的先done(合并回收,一步不省)。- 没人认领、零提交、尚未 claim 的空闲树 →
abort <ID>腾位(实测就是 abort 掉排在后面的空闲树换出名额)。abort= 工作树 + 分支一起删、永久丢弃;工作树有未提交改动或已有提交时脚本会拒绝,确认可弃再加--force。 - 确实要同时开第 9 棵才
new <ID> <slug> --force:磁盘代价约 83 MB/棵(--lean约 45 MB), 并行度上去了、冲突面与台架排队也一起上去——不是免费的。 - 别用手工
git worktree remove给「登记任务」腾位:台账不会被删(下次new同 ID 会报「任务 已存在」), 还绕过了「先摘联接」(铁律 5),有让 git 递归进联接把主工作树.venv删掉的风险。要回收的若是非登记树 ($TW list里根本没有它:new出来又被手工删掉只剩悬挂台账的残骸 / D 节--detach净室 / 别的会话手工建的) → 腾位与回收一起走 E 节,那里有「怎么证明这棵树零联接所以可以 remove」的可执行判据。 - 腾位后复验:
$TW list的条目数与ls _wt的目录数一致,再new。
B. 非登记持有者占的锁:lock release --bench-pass 曾报「任务 X 不存在(缺台账)」
症状(lead 在主工作树跑台架验收窗,用临时名占锁 lock acquire bench --task acceptwin):
@T3@bash "$PY" "$TW" lock release bench --task acceptwin --bench-pass 前置/环境错误:任务 acceptwin 不存在(缺台账 _wt/.state/acceptwin.json) # 锁照旧挂着 "$PY" "$TW" lock release bench --task acceptwin --force --bench-pass # 同一句错误,锁仍在 @T3@
根因:release 分支里只要带 --bench-pass 就先 load_state(task) 去台账写 PASS,而 acquire 侧
不查台账——这个不对称让「释放」被「记账」挡死(--force 只覆盖持有者不符与陈旧接管,管不到台账)。
后果比报错本身大:锁没放,done 的前置 release_stale_locks() 对未到 TTL 的活锁直接判失败 →
一个名字没登记,就挡住所有任务的收口。
2026-10-29 起已修(task_worktree.py:无台账降级为 [warn] …跳过 PASS 记录,仅释放锁 并照常释放)。
现在的正解只有三步,不再需要手写占位台账:
$TW lock status认现场(持有者 / pid / 已持有时长 / TTL)——别对同一句报错反复重试 release。$TW lock release <build|bench> --task <NAME> [--bench-pass]直接放;PASS 记不上会打印 warn,锁照放。- PASS 后面还要用(
done只认台账里的bench_pass)→ 从一开始就用已new出来的任务 ID 占锁, 或把验收结论写进主干提交说明 / 文档。插件树pw:<name>天然没有台账,别指望它留 PASS → F 节。
最后手段(脚本本身都走不通时):锁就是 _wt/.state/locks/<name>.lock 里的单行 JSON
(name/task/pid/host/epoch/at/note/ttl),release 的实现就是 unlink 它——手工删这个文件等价于
「不记 PASS 的强制释放」,只在前面几步都不可用时才用。历史遗留的占位台账(早期为绕这个坑手写的
_wt/.state/<NAME>.json)现在已无必要:它活着时既占 MAX_CONCURRENT = 8 名额又被 list 当任务列出,
按 E 第 6 步备份后删掉即可。
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] static: pre-commit hooks -- passed=N failed=M, 不同根因处置不同(见陷阱「done的门禁 =pre-commit run --all-files」)。最常态是.map滞后 → 第 5 步。
- 补绿固定链(2026-10-25 实测;顺序即因果,全在主工作树跑,逐条不接管道看退出码):
@T3@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 边界),按用户指示再做 @T3@
为什么必然要补,见陷阱「done 的门禁跑在 @build 之前」。
6. 重跑 done 之前主工作树必须干净——否则前置检查直接
失败:主工作树不干净(…)——合并会把未提交改动卷进来,所以第 5 步的产物必须先提交。重跑时若分支
已并入,会打印 [i] task/<ID>-<slug> 已合入 <base>,跳过合并,直接清场 → 摘联接 → 完成(分支此前已合入)。
7. 清场完成后再敲一次 done <ID> 会得到 前置/环境错误:任务 <ID> 不存在(缺台账 …json)(exit 2)——
这不是失败:清场的最后一步 unlink 了台账。判完成以 git log 为准。
D. 门禁指标异常增长 → 逐文件归因(把净室工作树当测量仪,主树全程只读)
场景(2026-10-27 实测):id-check 突然 FAIL「不可判定 1,178 > 上限 1,166」——比上一轮量到的 1,162 又涨了 +16,
而主工作树里同时压着 4 条在飞任务道的未提交改动。不许 stash / checkout / reset(那是搬动甚至丢弃别人未提交的工作,
项目级 Never;git status 一乱,整条道的现场就没了),不许改门禁脚本本身(它就是那把尺子,改了数字就不可信),
也别在脏主树里跑门禁(那测的是 4 条道 + 未跟踪文件的混合体,永远分不出谁)。正解:在仓库之外另起一棵
detached HEAD 的干净工作树当净室,一次只往里注入一份在飞文件,与基线差分即得每个文件的独立贡献。两步:
净室建树 → 单文件注入测量。
- 建净室 + 先落基线数(路径必须在仓库目录之外;铁律 7 对临时树同样成立——放仓内就污染
git status/pre-commit --all-files/ 全库搜索):
@T3@bash MAIN="D:/Code/DTU Code/STM32F4-Base" # 主工作树绝对路径(含空格 → 变量整体加引号) WT="$MAIN/../_wt/und-attr" # 仓外同级 _wt/ 下;一次测量固定用同一个名字 rm -rf "$WT" 2>/dev/null git -C "$MAIN" worktree add --detach "$WT" HEAD >/dev/null 2>&1 || { echo WT_FAIL; exit 9; } git -C "$WT" status --porcelain # 应为空 "$PY" "$WT/scripts/archive/id_gate/check_ids.py" 2>&1 | tail -25 # ← 口径:id-check 钩子已于 2026-10-28 退役(脚本移入 scripts/archive/id_gate/),按该脚本自身默认参数跑;基线与注入用同一条命令 @T3@
--detach(不要-b):净室没有分支、不写_wt/.state/<ID>.json台账 → 不占MAX_CONCURRENT = 8名额、不出现在$TW list,也没人会被误指去对它跑done(建与拆都不走$TW new/$TW done)。- 能这么量的前提:该门禁只读仓库内文本。依赖本树构建产物的门禁(
mem-numbers/mem-budget要.map) 在净室里必然报「找不到 .map 事实源」(见陷阱「新建工作树从未构建过」)——那类指标不能这么归因。 git status --porcelain非空 = 有别人的未跟踪文件被带进净室 → 先git clean -xdn干扫(别去n) 看清会删什么,只挪走多出来的那些再测;语料被污染出来的数字与 CI 不可比。
- 一次只注入一份在飞文件 → 跑同一个扫描器 → 与基线差分(得到的是每个文件的独立贡献,不是合计):
@T3@bash for f in "App/log/sys_log_mcu.c" "test/host/sys_log_mcu/sys_log_mcu_test.c" "docs/子系统设计/运行与自检记录设计说明.md"; do mkdir -p "$WT/$(dirname "$f")" cp "$MAIN/$f" "$WT/$f" # 拷进净室;主树那份只读、一字节不动 echo "== $f =="; "$PY" "$WT/scripts/archive/id_gate/check_ids.py" 2>&1 | tail -25 # ← 与第 1 步同一条命令 git -C "$WT" checkout -- "$f" # 只还原净室里自己刚拷进来的那份 done @T3@
实测:run_par.c / board_ui_nav.py / check_nested_comment.py / keil SKILL.md 各 +0,
而 log 道那三个文件 +6 / +8 / +2,合计正好 +16。Σ(逐文件增量) ≠ 观察到的涨幅 → 还有没被注入的
在飞文件,或存在文件间交互项,不许就此交卷。
3. 定性增量是哪一类字面量(只有「谁」没有「为什么」= 给不出可执行的改法):同一计数器在 HEAD 版与
在飞版各数一遍,差值才是新增行带来的(存量的不算这条道的账);再对新增行直接 grep。本次 16 处全是
裸形 「字母 T 累进数字 4」(T 在 scripts/archive/id_gate/id_registry.json 的 bare_check_letters 里,规范形态是连字符形),
全部落进 skip_bare_unmanaged —— 到这一步结论才配发给属主。
4. 闭合 + 复验主树一个字节没被污染:
@T3@bash git -C "$MAIN" worktree remove --force "$WT" # 净室里只有自己的拷贝、没有原件,--force 安全 git -C "$MAIN" worktree list # 不该再有 und-attr git -C "$MAIN" status --porcelain | sort # 与测量前逐行一致(测量前先存一份基线清单) @T3@
⚠ 上面第一行的前提是有条件的:树里混进了别人的在飞 / 未跟踪内容时「只有自己的拷贝」就不成立了,
--force 直接拆等于替别人把唯一版本删掉 → 先走 E 节(全景 → 逐文件分桶 → 联接预扫 → 唯一残留先备份),
再回来执行本步;复验按 E 第 5 步做(拆前/拆后主树 status --porcelain -uall 行数必须相同)。
同一棵净室树的第二种用法:按指定 commit 只读构建出像 / 烧录验收——不搬动任何在飞改动,也不为台架
提前抢发一笔提交;构建与烧录都在这棵树里做(不在主树),仍受 bench / build 锁串行(铁律 3)。
E. 回收非登记工作树($TW list 为空 / detached HEAD / 树里有在飞或未跟踪内容)
这类树的判定特征(命中任意一条就适用本节):git worktree list 里有它、但 _wt/.state/<ID>.json 台账不存在;
HEAD 是 detached(git rev-parse --abbrev-ref HEAD 打 HEAD);或树里的改动不属于本会话(别的会话留下的未提交 /
未跟踪文件)。典型来源:D 节的 --detach 净室、new 之后被手工删目录只剩台账的残骸、别人手工 git worktree add 建的分析树
(2026-10-28 实测一次回收 3 棵:verify-task16 / verify-batch / und-attr,各 89 MB,全部无台账 + detached)。
为什么 done / abort 在这里整体不适用(铁律 9):两条子命令的前置都是 load_state(),无台账 →
前置/环境错误:任务 <ID> 不存在(缺台账 …json)(exit 2)当场停住、什么也不会发生;而假如有一份残留台账,
abort --force 会拿台账的 path 直接 git worktree remove —— 绕过下面第 1~4 步的全部内容保全,把别的会话的唯一版本一次删光;
done 则会去找那条(多半已不存在的)task/ 分支做合并。另:本路径没有「合并」这一步,所以 done 的并集合并残留审计
在这里不发生,也没有三方对比可依据 —— 唯一性只由第 2 步的内容分桶给出。回收只能手工走本节第 1~5 步 + 第 6 步清台账。
- 建全景(全程只读):先看
git worktree list的全集,再逐棵记三件事(脏行数含未跟踪 / 树顶提交 / 有无分支) 加一条台账判定,同时存下主树的拆前基线清单:
@T3@bash MAIN="D:/Code/DTU Code/STM32F4-Base"; WTROOT="$MAIN/../_wt" git -c core.quotePath=false worktree list # 全集:登记树 + 非登记树都在这份里 MAIN_SHA=$(git -C "$MAIN" rev-parse --short HEAD) # 下面所有比对都以这一版为主线尺子 mkdir -p ci-artifacts git -C "$MAIN" status --porcelain -uall > ci-artifacts/wt-main-before.txt # 拆前主树基线(第 5 步逐行比) wc -l < ci-artifacts/wt-main-before.txt # 记下行数(实测 0) for t in "$WTROOT"/*/; do echo "--- $t" git -c core.quotePath=false -C "$t" status --porcelain -uall | wc -l # 脏 + 未跟踪行数 git -c core.quotePath=false -C "$t" log --oneline -1 # 树顶提交 git -C "$t" rev-parse --abbrev-ref HEAD # 打 HEAD = detached,压根没分支可 done ls "$WTROOT/.state/$(basename "$t").json" 2>/dev/null || echo "NO-LEDGER" # 台账位置随 DTU_WT_ROOT done @T3@
-uall不是可选:默认status --porcelain会把未跟踪目录折叠成一行dir/,-uall才展开到文件级;少了它 第 2 步的分桶就漏项(实测und-attr唯一那份就是未跟踪?? scripts/check_nested_comment.py)。- 路径含空格(本项目
DTU Code)→ 变量整体加引号;core.quotePath=false让中文路径原样输出,便于逐行核对。 - 判定「是不是登记树」看两样:台账在不在 + HEAD 是否 detached。两者都指「非登记」→ 从这里开始禁止
done/abort(见本节开头),继续第 2 步。
- 逐文件与主线 HEAD 比内容并分三桶:
IN-MAINLINE(内容等价,拆 = 零损失)/UNIQUE-UNMERGED(主线没有这份 内容,拆 = 丢)/OLDER-DRAFT(同路径主线已有更新版本)。落成独立只读探针跑(别用 heredoc 现写现改, 见陷阱「别用 heredoc 打补丁造探针」):
@T3@python
ci-artifacts/wt_bucket_scan.py — 只读:逐文件与主线工作区/HEAD 比对,三桶归类
import os, subprocess, tempfile MAIN = r"D:\Code\DTU Code\STM32F4-Base" WTROOT = os.path.join(os.path.dirname(MAIN), "_wt") TREES = ["verify-task16", "verify-batch", "und-attr"] # ← 第 1 步全景里挑出的非登记树 SKIP_TOP = {".venv", "MDK-ARM", ".git", "ci-artifacts"} # 联接 / 构建产物 / 忽略区,按项目调 def git(*a): return subprocess.run(("git", "-c", "core.quotePath=false", "-C", MAIN) + a, capture_output=True, text=True, errors="replace") def bucket(rel, tree): p = os.path.join(tree, rel) if not os.path.exists(p): return "OTHER", "工作树里已不存在(多为已 staged 删除)" tf = tempfile.NamedTemporaryFile(delete=False) tf.write(open(p, "rb").read()); tf.close() # 1) 与主线工作区(= 当前 HEAD 的检出内容)逐字节比 if git("diff", "--no-index", "--quiet", tf.name, rel).returncode == 0: os.unlink(tf.name); return "IN-MAINLINE", "与主线 HEAD 逐字节相同" # 2) 与主线历史各版比:内容等价于某个旧 commit → 只是主线又往前走了 hist = [x.strip() for x in git("log", "--format=%H", "HEAD", "--", rel).stdout.splitlines()] for sha in hist[1:]: old = git("show", f"{sha}:{rel}") if old.returncode == 0 and old.stdout == open(p, encoding="utf-8", errors="replace").read(): os.unlink(tf.name); return "IN-MAINLINE", f"内容等价于主线历史版 {sha[:8]}" # 3) 主线从没这条路径 → 唯一;否则同路径主线有更新版 → 更旧草稿(再用 blob diff 定性方向) os.unlink(tf.name) if not hist: return "UNIQUE-UNMERGED", "主线 HEAD 与历史都没有此路径" return "OLDER-DRAFT", "同路径主线有更新版本(用 diff --stat <HEAD> -- 定性)" for name in TREES: tree = os.path.join(WTROOT, name) lines = subprocess.run(("git", "-c", "core.quotePath=false", "-C", tree, "status", "--porcelain", "-uall"), capture_output=True, text=True, errors="replace").stdout.splitlines() tally = {} for line in lines: rel = line[3:].split(" -> ")[-1].strip('"') # 兼容 ?? / M / A / R 的箭头形态 if rel.split("/")[0] in SKIP_TOP: continue b, why = bucket(rel, tree) tally.setdefault(b, []).append((rel, why)) g = lambda k: len(tally.get(k, [])) print(f"== {name}: IN-MAINLINE={g('IN-MAINLINE')} UNIQUE-UNMERGED={g('UNIQUE-UNMERGED')} " f"OLDER-DRAFT={g('OLDER-DRAFT')} OTHER={g('OTHER')}") for b in ("UNIQUE-UNMERGED", "OLDER-DRAFT", "OTHER"): for rel, why in tally.get(b, []): print(f" {b}: {rel} # {why}") @T3@
报告里必须逐桶给计数 + 判定证据(实测:verify-task16 IN-MAINLINE=6 / UNIQUE=0;verify-batch IN-MAINLINE=35 /
UNIQUE=1;und-attr IN-MAINLINE=0 / UNIQUE=1)。两个非空桶再各钉一条命令把证据坐实:
@T3@bash
「唯一未合并」是真更新还是更旧草稿?blob 级 diff + 逐行看方向
git -c core.quotePath=false -C "$MAIN" diff --stat "$MAIN_SHA" -- simulator/tests/README.md diff <(git -C "$MAIN" show "$MAIN_SHA":simulator/tests/README.md) "$WTROOT/verify-batch/simulator/tests/README.md"
「树顶提交要不要保」只看一件事:是否已并入 HEAD。别拿「目录很久没动」当判据
for c in 77779d49 1d4b6a17; do
git -C "$MAIN" merge-base --is-ancestor "$c" HEAD && echo "$c MERGED" || echo "$c NOT-merged"
done
@T3@
实测:那份 simulator/tests/README.md 树内写「27 页 × 3200 B」、主线 HEAD 已是「60 张 × 3200 B」→ 更旧草稿(被取代的
早先版本),不是未合并成果;bench-21-77779d49 / bench-23-1d4b6a17 两棵 merge-base --is-ancestor 判 MERGED 且脏数 = 0
→ 保留不动(不属本节,等属主自己收,报告里点名说清为什么留)。
回收判据不是「看着旧」:唯一内容如果没被提交过,git log 里根本看不见它;反过来「提交已 MERGED」也不等于可以拆——
还得看未跟踪那一层。UNIQUE-UNMERGED 非空又确实要留 → 它进第 4 步的备份,不许靠 --force 顺手带走。
3. 拆前扫联接 / reparse point(物理安全性 = 铁律 5 的可执行判据)。存成 ci-artifacts/wt_junction_scan.py 再跑:
@T3@python
只读预扫:一个路径是不是 reparse point(Windows 目录联接在这里现形);发现即停止深入,绝不 walk 进联接
import os, ctypes FILE_ATTRIBUTE_REPARSE_POINT = 0x400 GA = ctypes.windll.kernel32.GetFileAttributesW def is_reparse(p): a = GA(p) return a != -1 and bool(a & FILE_ATTRIBUTE_REPARSE_POINT) for name in ["verify-task16", "verify-batch", "und-attr", "bench-21-77779d49", "bench-23-1d4b6a17"]: top = os.path.join(r"D:\Code\DTU Code_wt", name) hits, entries = [], 0 for root, ds, fs in os.walk(top): bad = [d for d in ds if is_reparse(os.path.join(root, d))] if bad: hits += [os.path.join(root, d) + "/" for d in bad] ds[:] = [] # 有联接就不再深入:防转进主树 .venv 把别人整棵树数一遍 hits += [os.path.join(root, f) for f in fs if is_reparse(os.path.join(root, f))] entries += len(ds) + len(fs) print(f"== {name} scanned entries={entries}") print(" REPARSE: none → 可直接 git worktree remove --force" if not hits else " REPARSE:\n " + " ".join(hits)) @T3@
实测输出 == verify-task16 ... REPARSE: none(每棵的体积 / 条目数 + 零联接就是证据),而 bench-* 两棵各挂一个 .venv
联接 → 判 KEEP(本节不拆它们;将来真要拆,先 rmdir 摘掉联接再 remove)。
判据是「扫出来一个都没有」,不是「我记得没建过联接」:new --no-bootstrap 的树、别的手工 mklink /J 的树都可能带着指向主树的联接。
摘联接用 rmdir(只删联接本身;不是 rd /s,更不是 rm -rf——那会顺着联接删目标)。
非 Windows 环境改用 os.path.islink / find <tree> -type l(本项目是 Windows + Keil 工具链,按上面这条走)。
到这里才算「拆前唯一性自检 + 物理安全预扫」齐:--force 的安全前提 = 第 2 步 UNIQUE-UNMERGED 为 0
(或剩下的已全部进第 4 步备份)且这里 REPARSE: none。
4. 唯一内容先备份,再 --force 拆。UNIQUE-UNMERGED 与需要留档的 OLDER-DRAFT 一律先备份进主树
ci-artifacts/wt-leftover-backup/(ci-artifacts/ 已被 .gitignore 忽略 → 备份不会改变主树脏行数;仓库根不放一次性产物),
文件名 + 字节数必须打进输出当证据:
@T3@bash BK="$MAIN/ci-artifacts/wt-leftover-backup"; mkdir -p "$BK"
命名约定:<树名>__<原路径里的 / 换成 _>,反向可定位;台账备份统一后缀 .state-bak
cp "$WTROOT/verify-batch/simulator/tests/README.md" "$BK/verify-batch__simulator_tests_README.md"
cp "$WTROOT/und-attr/scripts/check_nested_comment.py" "$BK/und-attr__scripts_check_nested_comment.py"
ls -l "$BK" # 实测:verify-batch__simulator_tests_README.md 1769 bytes / und-attr__scripts_check_nested_comment.py 14827 bytes
for t in verify-task16 verify-batch und-attr; do
git -C "$MAIN" worktree remove --force "$WTROOT/$t"; echo "t remove_rc=?" # 不接管道才读得到码(见「读输出」)
done
git -C "$MAIN" worktree prune # 顺带清 .git/worktrees 里的残籍
@T3@
自检:备份条目数 = 第 2 步里判「要留档」的条目数,且每个都带字节数。这一步就是本节相对 D 第 4 步的增量——
D 的前提是「净室里只有自己的拷贝」,本节的树里混着别人在飞的东西,--force 是一次不可逆删除,先备份才谈得上安全。
5. 复验:主树一个字节没动 + 树确实没了:
@T3@bash git -C "$MAIN" status --porcelain -uall > ci-artifacts/wt-main-after.txt wc -l < ci-artifacts/wt-main-before.txt; wc -l < ci-artifacts/wt-main-after.txt # 两个数必须相同(实测 0 → 0) diff ci-artifacts/wt-main-before.txt ci-artifacts/wt-main-after.txt # 空输出 = 逐行一致 git -c core.quotePath=false -C "$MAIN" worktree list # 已拆路径逐条消失;主树与判「保留」的树原样在列 @T3@
行数不同 = 备份或摘除碰到了主线(多半是备份写进了未被忽略的目录,或漏摘联接)→ 立刻定位,别报「回收完成」。
第 2 步判「保留不动」的树还在 worktree list 里是预期结果,在报告里点名解释为什么留(附 MERGED 判据)。
6. 顺手清悬挂台账(branch / path 都已不存在的 _wt/.state/<ID>.json;它会让 $TW list 打出一行怪条目、并让下次
new 同 ID 报「任务 已存在」)。先给证据、后动手:
@T3@bash for j in "$WTROOT"/.state/*.json; do # 台账位置随 DTU_WT_ROOT id=$(basename "$j" .json) br=$("$PY" -c "import json,sys;print(json.load(open(sys.argv[1],encoding='utf-8')).get('branch',''))" "$j") pp=$("$PY" -c "import json,sys;print(json.load(open(sys.argv[1],encoding='utf-8')).get('path',''))" "$j") git -C "$MAIN" rev-parse --verify "$br" >/dev/null 2>&1 && b=branch-OK || b=branch-MISSING [ -d "$pp" ] && p=path-OK || p=path-MISSING echo "$id $b $p <- $j" done @T3@
实测这条判据打出的正是 v5tool2 branch-MISSING path-MISSING:git rev-parse task/v5tool2-v11 报
fatal: ambiguous argument 'task/v5tool2-v11': unknown revision ...,台账 path 指的目录已无,git worktree list 里也没有这棵树。
三条齐(rev-parse fatal + path 目录不存在 + worktree list 无此树)才叫悬挂;缺任何一条都回到第 1~2 步查是不是真有东西要保。
然后备份 → 删 → 复跑两条 list 复验:
@T3@bash
cp "$WTROOT/.state/v5tool2.json" "$BK/v5tool2.json.state-bak" && rm "$WTROOT/.state/v5tool2.json"
"$PY" "TW" list # 该条目消失(全空时打「(没有任务空间。用 new <ID> <slug> 建一个)」),echo RC=? 应为 0
"$PY" "$TW" lock status # build 空闲(TTL 1.0h)/ bench 空闲(TTL 6.0h)—— 证明清台账没牵连两把锁
ls "$WTROOT"/.state/locks/ 2>/dev/null # 若该 ID 名下还有 .lock,先按 B 节释放,再删台账
@T3@
- 必须
cp先备份再rm:台账里存着 base / junctions / needs_bench / bench_pass,是真出纠纷时唯一的现场记录。 - 悬挂条目按定义
path已不存在 → 通常不占MAX_CONCURRENT = 8名额(A 第 1 步的「活跃」判据要求 path 目录还在), 但它把$TW list搅成怪输出、并永久挡住同 ID 复用;path 还在的占位/残留台账才占名额(早期为绕 B 节那个坑手写的占位台账就是这种)。两种都要清。 - 仍不对这个 ID 跑
done/abort:清场会拿台账里的死path/ 死branch去做git worktree remove和删分支 (这正是 A 第 5 步说的「手工删目录留下的尾巴」,正解就是本步)。历史遗留的占位台账同理——活着时也别done/abort,用完直接删文件(来历与现状见 B 末条)。
F. 与 DSH worktree 插件(/worktree、模型工具 worktree_create)共存
本仓现在有两套隔离空间。别混用,也别互相回收:
@task(本 skill) |
DSH worktree 插件(dsh-task-worktree) |
|
|---|---|---|
| 树在哪 | 仓库外 ../_wt/<ID>-<slug>/ |
仓库内 .dsh-worktrees/worktree/<name>/ |
| 谁建 | new <ID> <slug>(人来敲命令) |
会话选「Worktree模式」/ 模型调 worktree_create(零命令) |
| 台账 | _wt/.state/<ID>.json(本 skill) |
.dsh-worktrees/manifest.json(插件私有格式,别去改) |
| 分支名 | task/<ID>-<slug> |
worktree/<name> |
| 共享联接 | new 自动建 |
建树时没有 → 要 plugin adopt 补 |
| 回收 | done(预演 / 合并 / 门禁 / 清场)或 abort |
用户敲 /worktree finish|bring-back|remove|prune(模型碰不到收尾命令) |
为什么允许它把树建在仓内(铁律 7 的唯一例外):路径写死在插件里(WORKTREE_NAMESPACE = worktree
<repo>/.dsh-worktrees/),而暴露给模型的工具只有worktree_create/worktree_list/worktree_status三个,配置面改不动。兜法三件(均已落地):
.gitignore预置.dsh-worktrees/—— 插件首次create会往.gitignore追加一模一样的字面量,ensureIgnored()见到同名规则即跳过;不预置就等于让插件在会话中途改这份受控文件(diff 里冒出 一行"不是我加的")。- 全库扫描的排除面登记了它:
scripts/check_doc_links.py的SKIP_PARTS、tools/refgraph/gen_refgraph.py的EXCLUDE_DIRNAMES、scripts/check_layer_deps.py的os.walk兜底。 一份仓库副本被全库扫描吞进去 = 文档链接假红 + 配图生成物逐字节漂移砸在某位同事头上。 - 联接与锁归属由本节命令接上(下面三步)。
这棵树是不是插件的:git worktree list 里路径含 .dsh-worktrees/ 就是。$TW list 现在会在台账段
下面另起一段列出它们,联接列点名缺哪一样(已联 / 缺 .venv / 缺 SDL2 / 齐(实体SDL2))。插件树不占
MAX_CONCURRENT = 8 名额——回收不归本 skill,
硬计入只会让别人那棵关不掉的树把 new 挡死(new 会打印一行「另有 N 个插件工作树不计入上限」提醒并发代价)。
三步接上(<name> 可带 worktree/ 前缀,也可多段如 refactor/logging):
@T3@bash "$PY" "$TW" plugin list # 一览:分支 / 脏 / 缺哪个联接(缺项另起一行说后果) "$PY" "$TW" plugin adopt N21-dhara-isfree # 只补缺失的 .venv 与 simulator/SDL2 联接 "$PY" "$TW" plugin unadopt N21-dhara-isfree # 只摘联接(os.rmdir),绝不动目标内容与树里文件 @T3@
- 两条联接坏了的样子完全不同:缺
.venv→ci_local.py的四级解释器回退静默落到机器级 python(无 fontTools =generated-fresh假红的老根因,AGENTS.md 记着);缺simulator/SDL2→build_sim.bat当场报错(SDL2_DIR是裸set,没有 env 逃生口)。所以列表只说「已联」不够—— 它按缺的那一样分别给后果,adopt没补齐也只报「还不能算就绪」,不许说「现在可以跑」。 - adopt 只碰联接:已联的跳过;
simulator/SDL2若已是实体目录只告警不动;.dsh-worktrees/之外的路径 一律 exit 2(实测plugin adopt ".."被拒)——这道闸防的是把主树或_wt目录当插件树遮蔽掉真实内容。 - 锁归属现在认得出来:
@build在插件树里自持自释时,持有者名从 cwd 反推成pw:<name>(多段名压平成-),与plugin adopt打印的那个名字逐字相同(keil_build.py: plugin_tree_owner()↔task_worktree.py: plugin_lock_owner())→lock status不再一律落成build-cli。 - 上台架仍须手工占锁、用同一个名:
lock acquire bench --task pw:<name> --note <说明>→lock release bench --task pw:<name> --bench-pass。插件树没有台账 → 按 B 节只 warn、锁照常放。 - 命名建议:插件
<name>沿用@task的<ID>-<slug>形态(如N21-dhara-isfree),两套视图同名好对账; 但分支前缀必然不同(worktree/vstask/),别把两者说成同一个分支。 - 收尾不在这:
done/abort拿插件树名下手只会 exit 2(这是保护,别绕过去手工git worktree remove; 要拆先plugin unadopt摘联接,再由用户/worktree remove)。/worktree bring-back要求主工作树干净, 本仓常有在飞改动 → 提交或 stash 由用户定。反过来插件也不会碰_wt/(路径根不同,谁也删不掉谁)。
铁律
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 的验收窗、插件树pw:<name>)占了锁 怎么放 → B 节(2026-10-29 起无台账也只 warn、照常释放);插件树整体口径 → F 节。
- 单写者文档:
docs/任务规划/(整个文件夹)、AGENTS.md的任务表块、MDK-ARM/*.uvprojx、.pre-commit-config.yaml、内存快照 —— 任务期不在任务工作树里改,留到done统一处理。 - 清场必须先摘联接:脚本会先
os.rmdir掉.venv/simulator/SDL2联接再git worktree remove。手工清场时千万别直接git worktree remove——git 可能递归进联接, 把主工作树的.venv一起删掉。手工拆任何一棵树之前先跑 E 第 3 步的预扫,拿到REPARSE: none才动手。 - 活跃任务硬上限 8(脚本常量
MAX_CONCURRENT = 8,每棵约 83 MB 检出——du 口径,内容字节和 84.0 MiB →docs/环境与部署/并行任务工作树方案.md§4)。满了new直接 exit 2, 腾位顺序与--force的代价 → A 节;悬挂台账的清理与复验 → E 第 6 步。 @task的工作树放仓库同级_wt/,绝不在仓库内部——否则污染git status、pre-commit --all-files与全库搜索。唯一例外 = DSH worktree 插件写死的仓内.dsh-worktrees/(改不了;已用「预置 .gitignore + 三个全库扫描门禁排除」兜住,见 F 节)。自己新建的树一律走_wt/。done是合并点:属主没提交完就别done;合并提交一旦进了主干,只用「主工作树补提交」收尾, 不用reset --hard反悔 → C 节。- 非登记 / detached 的树绝不跑
done/abort:两者都以台账为前置,无台账直接 exit 2;带着残留台账时abort --force会绕过内容保全把别人的在飞改动连树一起删。回收走 E 节(全景 → 分桶 → 联接预扫 → 备份再拆 → 复验 + 清悬挂台账)。
陷阱(实测)
--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 步)。- ⚠
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)。
验证
$TW list:台账段条目数 =ls _wt的目录数且 ≤ 3;刚回收的任务与历史占位条目都不再出现 (插件树另起一段,不参与这条计数)。$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报「任务不存在」不必再追。- E 节回收:
git worktree list只剩主树 + 判「保留不动」的树(已拆路径逐条消失);拆前/拆后主树git status --porcelain -uall行数相同且diff为空(实测 0 → 0);ci-artifacts/wt-leftover-backup/里每个留档件都打印过 文件名与字节数、条目数 = 分桶判「要留档」的条目数;每棵被拆的树都留有REPARSE: none预扫记录;全景输出里UNIQUE-UNMERGED归零(或全部已备份);保留树在报告里点名给出判据(merge-base --is-ancestor= MERGED)。 - E 第 6 步清悬挂台账:该 json 已从
_wt/.state/消失(.state-bak在备份目录里)、$TW list不再打出它、$TW lock status两把锁仍空闲;全程未对该 ID 跑done/abort。 - F 节桥接:
plugin list的「联接」列按实际三态点名(已联/缺 .venv/缺 SDL2/齐(实体SDL2)——旧实现只取样.venv,半联树会说谎);adopt/unadopt 前后主树.venv与树内文件都 还在;主线缺 SDL2 时adopt打[skip]+ 「还不能算就绪」而不打「现在可以跑」;plugin adopt ".."必须 exit 2;打印的pw:<name>与keil_build.py从 cwd 反推的名字逐字相同;done <插件树名>只 exit 2。 - D 节归因:测量前后
git -C "$MAIN" status --porcelain逐行一致(主树全程只读);git worktree list里净室路径已消失;逐文件增量之和 等于观察到的涨幅(实测 +6+8+2 = +16),且每处增量 都指到具体字面量类别(裸形 「字母 T 累进数字 4」)与属主;取数走import check_ids读ck.stats,且门禁脚本自身git diff为空。 - 补绿链的每一条都自己
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双端注册) - DSH worktree 插件(人类命令
/worktree、模型工具worktree_create,树在仓内.dsh-worktrees/) 与本 skill 的分工、桥接命令与锁归属 → F 节(本 skill 不删插件树,插件也不删_wt/) .map口径 / RAM 台账与趋势 →mem-budgetskill;UI 回归 →ui-verifyskill- 完整机制与目录分配 →
docs/环境与部署/并行任务工作树方案.md