Files
STM32F4-Base/.dsh/skills/task-worktree/SKILL.md
T
wandering1 842d711ed6 refactor(编号): 测试验证层级去号——CI job / 门禁输出 / 命令行旗标换语义名 + ASCII 段名
用户裁定「我不想看到编号」。测试验证层级这一族的整族清退分三道并行:本笔是**代码与 CI 道**
(CI 步骤与门禁输出面),代码注释面另走、文档面由文档道走。

口径(旧 → 语义名 → ASCII 段名,段名只进日志/旗标/job id/文件名):
  L0 静态门禁 static | L0a 钩子解释器预检 preflight | L0b 提交信息门禁 commitmsg
  L0c 体积门禁 size | L0.5 主机侧纯逻辑单测 host-unit | L0.5c 覆盖率 coverage

- scripts/ci_local.py 43 站点:段标题与 [SUMMARY] 标签改 ASCII 段名(static: /
  host-unit:),函数 run_l0/run_l05 → run_static_gates/run_host_unit_tests,
  收尾「已跑:」列表用段名。旗标改 --static-only / --host-unit-only,**旧旗标
  --l0-only / --l05-only 保留为 argparse 别名**(肌肉记忆与既有脚本片段不断档,
  实测 --help 两名列出)。
- .gitea/workflows/ci.yml 42 站点:job id l0-gates → static-gates、
  l05-host-unit-tests → host-unit-tests;步骤名与 step-summary 标题改语义名;
  日志名 l0-pre-commit.log → static-pre-commit.log、l05-<模块>.log →
  host-unit-<模块>.log;::error::/::notice:: 文案同步。yaml.safe_load 核过
  两个 job id 与步骤名。
  host-unit-tests;步骤名与 step-summary 标题改语义名;日志名 l0-pre-commit.log →
  static-pre-commit.log、l05-<模块>.log → host-unit-<模块>.log;::error::/::notice::
  文案同步。yaml.safe_load 核过两个 job id 与步骤名。
- 门禁脚本 18 站点:check_registration(登记簿理由串 7)、check_test_coverage 5、
  README 8、check_generated_fresh 2、ram_baseline 2、check_size_gate 1、
  ci_find_python.ps1 2、ci_runner_bootstrap.ps1 2、.pre-commit-config.yaml 2、
  requirements.txt 1(保持纯 ASCII)。
- skills 7 站点:task-worktree(含 [SUMMARY] 样例两行)、mem-budget、ui-verify、
  task_worktree.py 的实跑标签。
- AGENTS.md 3 站点:CI 段「只跑静态门禁 + 主机侧纯逻辑单测」、自查命令段、
  「2026-10-11 静态门禁全灭」事件名。
- 归档:编号治理历史.md 新增 §10 换算表 + **撞族警示**(同形 L0/L1/L2/L3 还属 UI 设计
  令牌层 / UI 菜单层级 / 采集链 / 链路时延段 / CubeMX 三层 / 硬件位号与 FTP 应答字面量
  等六个族,其中 6 处在字符串字面量里,改了就是改行为——故全程只按行内置换、先判族)。
- docs/产品需求 §7.4 测试分层表:去号 + 顺带修掉上一批**错族替换疤**(该表 L1/L2 行曾被
  采集链层名「语义取数与有效性 / 全周傅氏相量」「保护判据」顶替,现按内容改写为板端测试
  套件 / 主机侧回归两行)。同文档 §1.5 G5 判据行同步。

门禁:ci_local --help 两面旗标并列;py_compile 7 个 .py 全过;yaml 解析通过;
check_ids_new --worktree rc=0;git diff --check 干净;17 个改动文件 FFFD=0、无 CR、
BOM 与原文件一致(含 requirements.txt 纯 ASCII)。本族无机器识别面(单字母+数字是
新增行门禁的自述盲区),口径来自自建逐行扫描器,复扫本族形态归零。
- 交接说明:文档道 teammate 在改 CI 指南时中断,剩余 9 处双写疤与 29 处 CI 标识
  (job id / 日志名 / 汇总行字面量)由 Lead 接手改完,逐处仍走断言式行内置换。
2026-10-28 17:46:52 +08:00

32 KiB
Raw Blame History

name, description, whenToUse
name description whenToUse
task-worktree 多任务并行开发的空间管理(git worktree 生命周期):new 开一棵工作树 + 任务分支、目录联接共享 .venv 与模拟器 SDK、活跃任务硬上限 3 与腾位、build/bench 两类独占锁(含非登记任务占锁后「缺台账」释放不掉的正解)、done 预演合并回主干、门禁未全绿的补基线收尾、并集合并残留审计,以及**把仓外 detached worktree 当净室测量仪**——门禁指标异常增长时一次只注入一份在飞文件、与基线差分做逐文件归因。规格见 docs/环境与部署/并行任务工作树方案.md。 要同时推进多个任务、担心同分支同文档互相冲突;要为新任务开一个隔离空间;要把任务合并回主干;或遇到 `new` 报「已有 3 个活跃任务(上限 3)」、`lock release` 报「任务 X 不存在(缺台账)」、`done` 报「门禁未全绿:工作树与分支保留,未清场」、两个任务抢 COM6 / UV4、"改了文档被覆盖"、并集合并残留时;或**门禁指标异常增长(如 id-check 的不可判定数从 1,162 涨到 1,178、+16)却查不出是哪个文件 / 哪条任务道造成的**、主工作树压着多条在飞改动而不许 stash / 提交 / 改门禁脚本、要按 HEAD 基线只读出像验收时。

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

读输出:管道与截断(两起误判的共同根因)

  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 会报「任务 已存在」), 还绕过了「先摘联接」(铁律 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 个):
{
  "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] static: pre-commit hooks -- passed=N failed=M, 不同根因处置不同(见陷阱「done 的门禁 = pre-commit run --all-files」)。最常态是 .map 滞后 → 第 5 步。
  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 为准。

D. 门禁指标异常增长 → 逐文件归因(把净室工作树当测量仪,主树全程只读)

场景(2026-10-27 实测):id-check 突然 FAIL「不可判定 1,178 > 上限 1,166」——比上一轮量到的 1,162 又涨了 +16, 而主工作树里同时压着 4 条在飞任务道的未提交改动。不许 stash / checkout / reset(那是搬动甚至丢弃别人未提交的工作, 项目级 Never;git status 一乱,整条道的现场就没了),不许改门禁脚本本身(它就是那把尺子,改了数字就不可信), 也别在脏主树里跑门禁(那测的是 4 条道 + 未跟踪文件的混合体,永远分不出谁)。正解:在仓库之外另起一棵 detached HEAD 的干净工作树当净室,一次只往里注入一份在飞文件,与基线差分即得每个文件的独立贡献。两步: 净室建树 → 单文件注入测量。

  1. 建净室 + 先落基线数(路径必须在仓库目录之外;铁律 7 对临时树同样成立——放仓内就污染 git status / pre-commit --all-files / 全库搜索):
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/),按该脚本自身默认参数跑;基线与注入用**同一条命令**
  • --detach(不要 -b):净室没有分支、不写 _wt/.state/<ID>.json 台账 → 不占 MAX_CONCURRENT = 3 名额、不出现在 $TW list,也没人会被误指去对它跑 done(建与拆都不走 $TW new / $TW done)。
  • 能这么量的前提:该门禁只读仓库内文本。依赖本树构建产物的门禁(mem-numbers / mem-budget 要 .map) 在净室里必然报「找不到 .map 事实源」(见陷阱「新建工作树从未构建过」)——那类指标不能这么归因。
  • git status --porcelain 非空 = 有别人的未跟踪文件被带进净室 → 先 git clean -xdn 干扫(别去 n) 看清会删什么,只挪走多出来的那些再测;语料被污染出来的数字与 CI 不可比。
  1. 一次只注入一份在飞文件 → 跑同一个扫描器 → 与基线差分(得到的是每个文件的独立贡献,不是合计):
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

实测: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. 闭合 + 复验主树一个字节没被污染:

git -C "$MAIN" worktree remove --force "$WT"   # 净室里只有自己的拷贝、没有原件,--force 安全
git -C "$MAIN" worktree list                   # 不该再有 und-attr
git -C "$MAIN" status --porcelain | sort       # 与测量前**逐行一致**(测量前先存一份基线清单)

「增量 + 字面量类别 + 属主」三者齐了才算归因完成。同一条道自己「我自查过不是我们」的结论别直接采信 → 陷阱「队友的自查结论要复核」。 同一棵净室树的第二种用法:按指定 commit 只读构建出像 / 烧录验收——不搬动任何在飞改动,也不为台架 提前抢发一笔提交;构建与烧录都在这棵树里做(不在主树),仍受 bench / build 锁串行(铁律 3)。

铁律

  1. done 只能在主工作树执行——主干分支检出在主工作树,git 不允许同一分支在两处检出。
  2. done 不 push。它结束于本地合入 + 门禁全绿;推送需用户明确许可(项目既有纪律)。
  3. build 与 bench 都必须串行:UV4 不能两个实例同时 -b;串口 COM6、ST-LINK、 设备 IP 192.168.1.100 各只有一份。构建与烧录都不可并发。
    • build 锁由 @build 自持自释,调用方无需手工 acquire/release;
    • bench 锁必须手工:lock acquire bench --task <ID> → 烧录/@test/探针 → 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(脚本常量 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, 不需要管理员权限),且联接必须建在版本控制之外。 实测(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] 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-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」四层,必被吞。
  • 取门禁数字: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 在清场前归并回主工作树。
  • --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-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 报「任务不存在」不必再追。
  • 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-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