Files
STM32F4-Base/docs/环境与部署/并行任务工作树方案.md
T
wandering1 7efa58d9a1
CI / 静态门禁 (pre-commit) (push) Failing after 25s
CI / 主机侧单测 (MSVC) (push) Successful in 48s
network: 台架 IP 迁到 192.168.31.100,并把重复写死的值收进单一真源
分两段(用户裁定 2026-11-02):先做地址迁移,再把同类字面量收敛成
单一真源,以后再改地址只动一处。

一、IP 迁移 192.168.1.100 → 192.168.31.100(gw 同步 → .31.1)
- 工厂默认:Drivers/BSP/NETCFG/net_cfg.c 的 NET_CFG_DEF_IP/_GW
- 注释/文档示例、主机与板级测试、台架 py 脚本、两份 sim_config.ini
- 字节序教学点同步换算 hex:0x6401A8C0 → 0x641FA8C0(内存
  C0 A8 1F 64),旧版示例引文与历史 Trap 记录按冻结纪律不动
- UI 黄金基准:网口设置 4 态 + shot_info 共 5 张 .bin 由
  check_content_pages.py --update 重刷(渲染值 IP 变了,属预期变更)
- 保留判定:客户点表引用(IEC104/继电保护规划)、芯片手册摘录、
  日期冻结的变更记录行、已退役台架档案 MDK-ARM/retracted_bench_*

二、单一真源收口(A+B+C+D,用户批准全做)
- 新建 test/host/bench_defaults.py:台架 py 默认值唯一定义点
  (DEVICE_IP/DEVICE_GW/PC_IP/NET_BCAST),22 个脚本改为引用;
  MDK-ARM 两探针与 scripts/board_ui_nav.py 经 sys.path shim 引入
- test/board/net_test_task.h 新增 TEST_PC_PEER_IP,板测三处
  PHASE*_REMOTE_IP 改引该宏(保留 -D 覆盖);Phase1 断言改
  memcmp 数组比较,数组各只出现一次
- simulator/tests/check_content_pages.py 从 net_cfg.c 正则解析
  NET_CFG_DEF_* 推导期望值(解析失败即报错、不静默兜底),
  脚本内台架 IP 字面量清零
- 装饰性注释去具体值(net_cfg.h/net_socket.h/net_socket.c/
  net_socket_sim.c);字节序教学 hex 对刻意保留为唯一写值点
- 刻意不动:主机单测夹具 IP(逐文件钉住的任意值是正确测试
  实践)、文档正文 IP(那是文档内容本身)

顺带:scripts/gen_task_table.py 规范串清掉「,B5」——编号清退提交
72b4b094 改了受管区没重跑生成器,task-table 门禁因此一直红

验证:产品 @build 0/0;CH395F 套件开关构建 0/0 后还原产品态;
6 套主机单测 rc=0;content_pages(含宏推导路径)/菜单 8 态/
推流位比对全 PASS;bom-drift 99 无漂移;doc-links/coding-standard/
cppcheck 绿;py_compile 23 文件 + --help 冒烟(含 shim 绝对路径)
2026-11-02 14:46:42 +08:00

443 lines
34 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 并行任务工作树工作流(worktree + `task-worktree` skill)
> **这个文件是什么**:多任务并行开发的**机制规格**——如何为每个任务开一个互不干扰的工作空间、
> 干完如何合并回主干、哪些资源必须串行、冲突怎么治理。
>
> **状态**:**已落地并端到端实测**(2026-10-12)——skill 在 `.dsh/skills/task-worktree/`,
> 实测记录与暴露并修复的缺陷见 §11。
>
> **决策基线**(2026-10-12 用户确认):工作树放**仓库同级 `_wt/`**;集成分支**沿用 `v2-on-ui`**;
> `done` 回收**不自动 push**(沿用「push 需用户明确许可」)。
---
## 1. 目的与范围
**要解决的问题**:多个任务同时推进时,若共用同一个工作目录,会互相踩——构建产物、日志目录、
Python 环境、以及 `docs/任务规划/` 这类**全局唯一登记簿**都会被并发写坏。
**做法**:每个任务一棵 `git worktree` + 一条任务分支,任务之间物理隔离;干完由固定命令合回主干。
**范围**:只解决「开发期隔离 + 回收期合并」的机械流程。**不改变**既有的分支纪律
(push 需许可)、门禁口径(pre-commit 52 钩子 + `/keil-build` + `/serial-boot-test`)与文档约定。
---
## 2. 方案选型
| 方案 | 为什么不用 |
| ---------------------- | ----------------------------------------------------------------------------------- |
| 同一目录切分支 | 构建产物 / `.venv` / 日志目录 / `.workbuddy-ai` 全共享,两任务互相踩;且本仓库明令禁止用 `git checkout` 丢弃未提交工作 |
| 每任务 `git clone` | 对象库 234 MB × N 份、Git LFS 全量重复;克隆之间无法安全共享分支,推送关系也更绕 |
| `git stash` / 打补丁倒换 | 不具备并发安全性,冲突只能靠人工记忆 |
| **`git worktree`(采用)** | 唯一同时满足「共享对象库 + 独立工作目录 + 独立 HEAD/index」的机制;git 原生,本仓库已有先例 |
**命名惯例来源**:`D:/Code/DTU Code/_v1pin-build`、`_v2oui-build` 曾是遗留 worktree(detached HEAD,
2026-09-19 建、已过期),本方案沿用其「仓库同级 + `_` 前缀」命名,但把散落的目录收敛到统一的
`_wt/` 容器下。**两者已于 2026-10-12 清理**:`_v1pin-build` 干净直接移除;`_v2oui-build` 有 2 个
未提交改动(uvprojx / freertos.c),已按字节留档于 `_wt/.archive/2026-10-12-legacy-worktrees/`
后才移除(两提交由两条 `wip/*` 保活分支持有,其一为 `wip/v2-pending-recv`;无提交丢失)。
---
## 3. 目录与资源分配
### 3.1 目录布局
```text
D:/Code/DTU Code/
├── STM32F4-Base/ # 主工作树 = 集成分支 v2-on-ui(唯一检出主干的地方)
│ ├── .git/ # 共享对象库 + hooks(worktree 自动共享,无需任何操作)
│ ├── .venv/ # 唯一实体(Python 环境)
│ ├── .workbuddy-ai/ # 唯一实体(记忆;**不**共享给任务树,见 3.2)
│ └── simulator/SDL2/ # 唯一实体(模拟器 SDK)
└── _wt/ # ★ 并行任务容器(不在任何 git 仓库内)
├── .state/<ID>.json # 任务台账:分支 / 联接 / 门禁与台架戳
├── .state/locks/ # 独占锁:build.lock(UV4)+ bench.lock(COM6/ST-LINK/设备 IP)
└── <ID>-<slug>/ # ← 一棵工作树(分支 task/<ID>-<slug>)
├── .git # 文件(指向共享对象库,git 自动创建)
├── .venv # → junction 到主工作树
├── simulator/SDL2 # → junction 到主工作树
├── .workbuddy-ai # 本任务自己的(清场前归并回主工作树)
└── MDK-ARM/… # 本任务独占的构建产物
```
**工作树绝不能建在仓库内部**(`STM32F4-Base/` 里):那会变成未跟踪目录,污染 `git status`、
`pre-commit --all-files` 与全库搜索。故统一放仓库同级的 `_wt/`。
### 3.2 共享 vs 独占
| 资源 | 归属 | 原因 |
| ------------------------------------------------------------------ | ---------------- | ------------------------------------------------------------- |
| `.git/` 对象库 + hooks | **共享**(git 机制自动) | 工作树共享 common dir,提交门禁无需在新空间重装 |
| `.venv/` | **共享**(junction) | 体积大、内容与任务无关;每任务重建一份纯属浪费 |
| `simulator/SDL2/` | **共享**(junction) | 下载型 SDK,与任务无关 |
| `.workbuddy-ai/` | **独占**(刻意不共享) | ① 避免并发写同一份日志丢更新;② 不给联接递归删除的机会——它是本项目最不可再生的目录。任务侧记忆由清场前归并回主工作树 |
| `MDK-ARM/` 构建产物、`simulator/build/` | **独占** | 正是需要隔离的部分:两个任务可各自构建互不干扰 |
| `.serialtest/config.json`、`.dsh/skills/`、`.pre-commit-config.yaml` | **独占但内容一致** | 受版本控制,随检出白拿,无需 bootstrap |
| `.mnemon/`、`ci-artifacts/` | **独占** | 本地运行态,gitignored |
### 3.3 junction 规则(**不能用 symlink**)
本仓库 `core.symlinks=false`:git 里的符号链接会被检出成「内容是目标路径的普通文本文件」,
不是链接。因此**共享 `.venv` 只能靠 Windows 目录联接(junction)**,且 junction 必须创建在
**版本控制之外**(`_wt/<ID>-<slug>/` 里的那几个路径都不在 git 索引内,安全)。
创建方式用 `mklink /J`(**不需要管理员权限**,也不同于符号链接)。实测(2026-10-12):
用 ctypes 直调 `DeviceIoControl(FSCTL_SET_REPARSE_POINT)` 会被系统拒绝(Win32 4392),
缓冲区经 hexdump 逐字节核对无误仍失败,而 `mklink /J` 正常 → 采用后者;参数以**列表**
交给 subprocess,含空格的路径(本项目 `DTU Code`)由 subprocess 自动加引号。
**摘除联接必须用 `os.rmdir`**(= `RemoveDirectoryW`,只摘链不递归)。实测 `.venv` 条目数
摘除前后一致(2809 → 2809)。**清场顺序铁律:先摘联接,再 `git worktree remove`**——
否则 git 可能递归进联接,把主工作树的 `.venv` / SDL2 一起删掉。
> ⚠ 不要在仓库里提交任何指向 `../` 的 symlink 来实现共享——在本机就是坏文件。
> ⚠ MSYS 的 `rmdir` 拒绝摘除联接(把它当符号链接),手工清场要用 Python 的 `os.rmdir`
> 或直接走本 skill 的 `abort` / `done`。
### 3.4 与 DSH worktree 插件的边界(2026-10-29)
本仓现在有**两套**隔离空间并存,这是**有意接受**的现状,不是待清理的重复建设:
| 维度 | 本方案(`task-worktree` skill) | DSH 插件 `dsh-task-worktree` |
|---|---|---|
| 树的位置 | 仓库外 `../_wt/`(§3.1) | 仓库内 `.dsh-worktrees/worktree/`(插件写死,不可配置) |
| 面向的粒度 | 任务(周级、要合并回主干、要过门禁) | 会话(一次对话一个空间,零命令起步) |
| 生命周期属主 | 本 skill 的 `new`/`done`/`abort` + `_wt/.state/` 台账 | 插件自己的 `manifest.json` + `/worktree` 收尾命令 |
| 分支命名 | `task/<ID>-<slug>` | `worktree/<name>` |
两条**并发红线由本方案单方守住**(插件不感知):`build`(UV4 单实例)与 `bench`(COM6 / ST-LINK /
设备 IP 唯一)这两把锁的位置与 TTL 口径见 §7.1 / §7.2——插件树里跑 `/keil-build` 时,`keil_build.py`
会照旧自持自释,持有者名从 cwd 反推为 `pw:<name>`;`bench` 仍须手工占同一个名。共享联接(§3.3)
插件不会建,由本 skill 的 `plugin adopt <name>` 补(执行体命令行见 §8)。
**为什么允许它在仓内破例**(与 §10「绝不在仓库内部」的口径冲突处):路径根写死在插件里,且暴露给
模型的工具只有 create/list/status 三个。兜法是把该目录登记进**全库扫描**的排除面(`check_doc_links`
的 `SKIP_PARTS`、`gen_refgraph` 的 `EXCLUDE_DIRNAMES`、`check_layer_deps` 的 `os.walk` 兜底)并在
`.gitignore` 里**预置**同名规则(否则插件首次建树时会往 `.gitignore` 追加一行,等于会话中途改受控文件)。
操作层的命令与判据 → `.dsh/skills/task-worktree/SKILL.md` F 节(本文只记边界,不重述步骤)。
---
## 4. 分支模型
| 项 | 取值 |
| -------- | ---------------------------------------------------------------------------------------------------- |
| 集成分支(主干) | `v2-on-ui` |
| 任务分支 | `task/<ID>-<slug>`,从主干当前 HEAD 切出 |
| `<ID>` | **沿用 `docs/任务规划/README.md` 的编号空间**(`P0-x`/`N-x`/`S-x`/`T-x`/`B-x`/`C-x`/`D-x`);无对应条目的临时任务用 `X-<日期>-<slug>` |
| 合并方式 | `git merge --no-ff` 合入主干 |
| 并发上限 | **同时最多 8 棵登记工作树**(2026-10-29 由 3 上调,用户裁定;另有插件树不计入,见 §3.4) |
**`<ID>` 复用登记簿编号**是为了可追溯:任务分支名直接对应待办条目,回收时销项有据可查。
**并发上限的依据**(2026-10-29 由 3 上调到 8,用户裁定;脚本常量 `MAX_CONCURRENT`):
磁盘从来不是这条线的硬约束——实测一棵纯净检出 **84.0 MiB / 2857 个受控文件**(含 LFS smudge
后的真图;`docs/` 独占 46 MiB),而 `.venv` 与 `simulator/SDL2` 走目录联接共享、不随树复制;同日 D
盘余 1.4 TB,**8 棵全开的最坏估算也只有约 3.2 GB**(每棵再叠一次构建产物:Keil 中间件 289 MB + 模拟器 35 MB)。
真正随并行数上涨的是**排队与合并**:`/keil-build` 与台架 `/serial-boot-test` 各被一把独占锁串行化(见 §5),`done`
的合并必须串行(§5 下方说明),而每多一棵树把 `sync` 追平与冲突面乘一遍。所以这条线现在是**管
理半径闸门**而非磁盘闸门:调到 8 = 把「同时盯几棵」交回人,两把锁的排队代价用 `$TW lock status` 与
§7 的台架台账观测;脑子里的名额够不够,看的是锁队列长度,不是磁盘。
---
## 5. 任务生命周期
| 阶段 | 动作 | 并行性 |
| ------ | -------------------------------------------- | ----------- |
| `new` | 建分支 + 工作树 + junction + 台账 | 可并行 |
| 编码 | 在新空间里改代码 | 可并行 |
| 单测产物 | `%TEMP%\dtu_host\<工作树哈希>\` 分家 + 每跑独占 `runs\<pid>-<ns>\` | **不需要锁,但必须按树分家**(旧固定共享名跨树互踩:A 树可能执行 B 树编出的同名 exe,summary 相同不可辨;build/bench 是真独占,继续用锁) |
| 门禁 | `scripts/ci_local.py`(静态门禁 + 主机侧纯逻辑单测,纯软件) | **可并行** |
| 构建 | `/keil-build`(UV4 增量,需持 `build` 锁) | **必须串行** |
| 台架 | `/serial-boot-test` / 板级套件(需先持台架锁,见 §7) | **必须串行** |
| 提交 | 本地 `git commit`(多行正文用 `-F <UTF-8 无 BOM 文件>`) | 可并行 |
| `sync` | `rebase` 追平主干,把冲突控制在任务分支上 | 可并行 |
| `done` | 预演 → 合入主干 → 残留审计 → 重生成产物 → 门禁 → 清理工作树 | **必须串行** |
| `push` | 推送主干/任务分支 | **需用户明确许可** |
**为什么 `done` 必须串行**:合并动作会移动主干引用,两处同时合入会互相覆盖;且主干只在主工作树检出。
---
## 6. 冲突治理
### 6.1 单写者文档
并行失败通常不是代码冲突,而是**多个任务同时改同一个「登记簿」**。以下文件在任务期间
**禁止在任务工作树里修改**,一律由 `done` 回收阶段在主工作树统一处理:
| 文件 | 冲突原因 | 回收时的处理 |
| ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| `docs/任务规划/`(整个文件夹) | 全项目唯一登记簿,每个任务都要销项 | 由主工作树统一改,任务分支不碰 |
| `AGENTS.md`、`docs/README.md`(索引/登记类) | 每个任务都要在 AGENTS.md 的「按主题查哪份文档」表、`docs/README.md` §9.2 环境表、`docs/README.md` 的文档索引表里**各加一行** → 同一张表的新增行两任务必撞(2026-10-12 实测:主工作树里本任务的行与他任务的行已并排混在**同一 hunk**)。变更记录那一路**不再是冲突面**:2026-10-28 起新文档一律不设变更记录节(`docs/文档规范.md` §5.1),存量也只剩 5 篇台账/证据文档 | 由主工作树统一追加;提交时**只 `git add` 本任务的行**(`git apply --cached` 选择性入库),勿整文件 `git add` |
| `AGENTS.md` 的 `TASK_TABLE` 块 | 生成物,两个任务都抬栈就冲突 | 合并后重跑 `scripts/gen_task_table.py` **重新生成**,不手工并 |
| `MDK-ARM/STM32F407-Demo.uvprojx` | 新源文件注册 + UV4 自动改写 | 手工并 `<File>` 条目 → 重跑 `registration-check` → `/keil-build` |
| `.pre-commit-config.yaml` | 新增主机单测要加钩子 | 并集处理 |
| `docs/内存管理/RAM使用分析与优化方案.md`(及其 `mem-numbers` 快照) | `mem-numbers` 快照 | **绝不手工合数字**,合并后 `scripts/check_mem_numbers.py --write` 重生成 |
### 6.2 合并残留审计(**必做**)
双方都改过的文件(尤其 `.gitignore`)会被 git 自动并成**并集**,可能留下:
- **死规则**:指向已删目录的忽略项
- **重复条目**:同一规则出现两次
判据 = 拿 base / 两个 tip **三方对比**(`git show <ref>:<file>` 逐项 `grep -F`),
**不要用 `tail -N` 目测**(会漏看被合并在中间的行),也**不要把合并前就存在的历史重复算进来**。
### 6.3 冲突时的解决位置
`done` 先做**非破坏性预演**:`git merge-tree --write-tree v2-on-ui task/<ID>-<slug>`。
- `rc=0`(无冲突)→ 继续合并
- `rc≠0` → **在任务分支上** `git merge v2-on-ui` 解决冲突并提交,再重跑 `done`
在一次性分支上解冲突,**永不污染主干**。
---
## 7. 资源串行化
### 7.1 台架锁
以下物理资源**各只有一份**,故 `/serial-boot-test` / 板级套件 / 烧录必须串行:
- 串口 `COM6` @115200
- ST-LINK SN `0B250D12A218303030303032`
- 设备 IP `192.168.31.100`
机制:`_wt/.state/locks/{build,bench}.lock` 记录持有者(任务 ID + pid + 时间戳 + 备注 + 自报 TTL)。
**锁不绑定进程存活**:`lock acquire` 本身是短命进程,而真正用资源的烧录/构建在另一个进程里跑。
若按 pid 判活,acquire 一退出锁就被判为陈旧 → 锁形同虚设(实测踩到)。
故判「陈旧」只看**时效**(该锁的 TTL)加显式 `--force`;pid 仅作排障记录。
同任务重复 acquire 幂等;他人持锁时 acquire 拒绝(`--wait` 可轮询等待)。
**TTL 按锁分设**(2026-10-12):`LOCK_TTL_HOURS = {"build": 1.0, "bench": 6.0}`。
口径 = 兜底窗口应与**操作真实时长**同量级:编译是秒~分钟级(`keil_build.py` 自持自释,
窗口 ≈ UV4 运行时长),1h 已足够宽松;bench 是**会话级**(flash→`/serial-boot-test`→探针),才配得上 6h。
持有者还可用 `lock acquire ... --ttl <小时>` **自报**更精确的窗口——
`keil_build.py` 即按其 `--timeout` 自报(`timeout/3600 + 15min`,下限 0.5h)。
`done` 前置检查:**陈旧锁自动接管释放**(不再硬失败),仅当锁仍被有效占用时才拒绝回收。
> ⚠ 台架相关的既有陷阱(`--require-selftest-pass` 的陈旧台账、回读校验会复位目标)
> 不因引入工作树而改变,仍按 `docs/测试说明/流程与规范/自动化测试流程.md` 执行。
### 7.2 构建
`/keil-build`(UV4 增量)在新空间里是**独占目录**,但 **UV4 不能两个实例同时 `-b`**(2026-10-12 用户确认,
无需实测)——因此构建**同样纳入串行锁**(`build`),与台架锁并列。
> 结论:任务期真正可并行的只有**编码与静态门禁**(`ci_local.py` 的静态门禁 / 主机侧纯逻辑单测,纯软件、零硬件);
> `/keil-build` 与烧录/台架都必须排队。
### 7.3 并行道与写范围规则(规划层 ↔ 执行层映射)
规划层(当前任务卡在 `docs/任务规划/UI补齐.md`,后续事项在 `docs/任务规划/登记簿.md`;
五字段 = **DoD / 硬依赖 / 资源 / 写范围 / 状态**)
按**资源属性**把任务切进三条道,本方案(工作树 + 锁)是它的执行层:
| 道 | 资源 | 并行性 | 锁 / 机制 |
|---|---|---|---|
| **台架道** | COM6 / ST-LINK / 设备(§7.1) | **串行**——约束资源,**永不空转**(约束优先,TOC):代码道在跑时必须为下一个台架任务备料 | `bench.lock` |
| **代码道** | CPU / 源码 | **并行**——worktree 独立空间;前提 = 写范围互不相交 + 强门禁(`ci_local` 跑满声明的 52 根、含 manual 段,口径 → `docs/门禁说明/README.md` §2;同 Linux 内核子系统树并行而能合回的逻辑,见 `docs.kernel.org/process/2.Process.html` 的 subsystem tree 模型) | 无锁;分支 ≤2 天、绿即合(trunk-based 短分支) |
| **外部窗口** | 标准源 / 第三方主站等外部设备 | **Fixed Date 类**(Kanban 服务分类):窗口一到全道让路;窗口前 2 日特性冻结(只修稳定,Zephyr RC 语义 → `docs.zephyrproject.org/latest/project/release_process.html`) | `bench.lock` + 外部预约 |
**四条规则**:
1. **WIP ≤ 2**:每道最多 1 个进行中(典型 = 台架道 1 + 代码道 1),先完再开——登记簿需可见
`⬜ 未开始 / 🟦 进行中` 两态,销项仍删行。
2. **写范围相交 = 声明合并次序**:任务卡必须写写范围(目录级);相交不排队等待,而是**声明谁先合**
(后合者在主工作树 rebase 后继续),claim 时定。
3. **分支存活 ≤2 天、绿即合**:合并走 `done` 预演(§6.3);单写者文档(§6.1)仍由主工作树统一处理。
4. **里程碑不拖延、功能顺延下一轮**(时间驱动):里程碑的退出判据写在登记簿,判据不满足则该**功能**
移入下一波次,里程碑本身按判据改期而非无限等待。
> 登记簿是规划层唯一权威(§2 铁律不变);本节只定义执行层映射,**不在本文排任务顺序**。
---
## 8. SKILL 设计
**名称**:`task-worktree` **文档别名**:`@task`——**只是文档记号,没有任何执行机制**:shell 里敲 `@task` 直接 command not found,`dsh task` 会被当 profile 名报错;DSH 会话里的显式调用是 `/task-worktree`,可执行形态只有下面的执行体命令行。
**执行体**:`.dsh/skills/task-worktree/scripts/task_worktree.py`(纯标准库,
照 `keil_build.py` 的写法,整目录可拷到别的项目复用)。**已落地并端到端实测**(见 §11)。
| 子命令 | 作用 |
| ----------------------------------------------------- | ----------------------------------------------------------- |
| `new <ID> <slug> [--base B] [--lean] [--needs-bench]` | 建分支 `task/<ID>-<slug>` + `_wt/<ID>-<slug>/` 工作树 + 共享联接 + 台账 |
| `list` | 所有任务一览:分支 / 落后领先 / 脏文件数 / 台架状态 + 两个锁的归属 |
| `status <ID>` | 单任务详情:未提交清单、相对基线漂移、台架记录、已建联接 |
| `sync <ID>` | `rebase` 追平主干,把冲突控制在任务分支上 |
| `lock acquire\|release\|status <build\|bench>` | 独占资源锁(`--task` / `--wait` / `--force` / `--bench-pass`) |
| `done <ID> [--dry-run] [--skip-build] [--skip-gates]` | 预演 → 合入主干 → 残留审计 → 重生成产物 → 门禁 → 清场 |
| `abort <ID> [--force]` | 放弃任务(有未提交/未合并内容时不加 `--force` 会拒绝) |
**环境变量**:`DTU_WT_ROOT`(任务容器位置)· `DTU_TASK_BASE`(默认基线)·
`DTU_INTEGRATION_WORKTREE`(集成点,默认主工作树;用于隔离演练/测试)。
**关键约束**:
1. **`done` 只能在主工作树执行**——主干 `v2-on-ui` 检出在主工作树,git 不允许同一分支在两处检出。
2. **`done` 前置检查主工作树干净**,脏则中止并提示(避免把未提交工作卷进合并)。
3. **`done` 不含 `push`**:结束于本地合入 + 门禁全绿,打印提示等用户点头。
4. `--needs-bench` 任务若没有台架 PASS 戳,`done` 拒绝合并。
5. 门禁未全绿时 **`done` 保留工作树与分支**(只留下合并提交),便于修正或回退——不静默清场。
6. 清场顺序:**先摘联接,再 `git worktree remove`**(见 3.3)。
---
## 9. 门禁与验收
| 层级 | 内容 | 时机 |
| ------- | ------------------------------------ | ------------------------------ |
| 任务期 | `scripts/ci_local.py`(静态门禁 + 主机侧纯逻辑单测,可并行) | 每个任务各自跑 |
| 任务期 | `/keil-build`(0 错 0 警,**持 `build` 锁串行**) | 每个任务各自跑 |
| **新空间首次** | `/keil-build` 一次,生成 `.map` | 见下表下注释:不先构建,体积类门禁必然红 |
| 任务期(可选) | `/serial-boot-test` / 板级套件(持 `bench` 锁) | 需真机验证的任务 |
| 回收期 | 重生成产物 → `ci_local.py` → `/keil-build` | `done` 内强制(`--skip-gates` 可跳过) |
| 人工 | push 许可 | 回收后 |
> ⚠ **新开工作树后先 `/keil-build` 一次再谈门禁**(实测 2026-10-25):`.map` 是每棵树各自的构建产物、
> 不入版本库,所以依赖它的事实源的门禁——`mem-numbers`、`mem-budget`、以及 `ci_local.py` 的
> 体积门禁——在从未构建过的新空间里**必然红**。这不是代码缺陷,脚本此时也会把这句原因打出来
> (`check_mem_numbers.py` / `ram_baseline.py` 的提示文案)。顺带一条同源提醒:`done` 的门禁
> 排在 `/keil-build` 之前(见下文陷阱条目),同一族误报在回收期也会重现一次。
**`done` 的完成判据**:主干上 `git merge --no-ff` 成功 + `ci_local.py` 全绿 + `/keil-build` 0 错 0 警
- 残留审计无死规则/重复条目 + 工作树已移除。
---
## 10. 已知风险与待验证项
| 项 | 状态 | 处置 |
| -------------------------------------------- | --------------------- | ------------------------------------------- |
| UV4 能否并发构建 | **已确认不可**(2026-10-12) | `build` 与 `bench` 一并纳入串行锁 |
| 跳过 Git LFS smudge 是否影响门禁 | **已实测**(2026-10-12) | 见下表:只省磁盘、不省时间,两种方式工作区都干净 → 默认全量,`--lean` 备选 |
| 主工作树当前不干净 | 已知 | `done` 的前置检查会拦下 |
| 遗留 worktree(`_v1pin-build` / `_v2oui-build`) | 已知 | 落地时 `git worktree remove` 清理,避免新旧两套并存 |
| **「主工作树绿、干净工作树红」的隐藏依赖** | **已实证**(2026-10-12) | 见 10.2。属**原任务**漏提交,非本 skill 缺陷 |
| `git worktree remove` 中途被中断 → 残留「裸目录」 | **已实证**(2026-10-12) | 见下方 10.3 |
### 10.1 LFS 实测数据(2026-10-12,2396 个受控文件 / 707 个 LFS `*.jpg`)
| 方式 | 建树耗时 | 检出体积 | `git status` | 参考图 |
| --------------------------------- | ------ | ----- | ------------ | ---------- |
| 全量 smudge(默认) | 7.25 s | 83 MB | 干净 | 真实 JPEG,可看 |
| `--lean`(`GIT_LFS_SKIP_SMUDGE=1`) | 7.05 s | 45 MB | 干净 | 指针文本 |
**结论**:LFS 对象已在本地,smudge 几乎不耗时,跳过**只省约 38 MB 磁盘**。
故默认走全量(保留参考图可读性),磁盘紧张时才用 `--lean`。
另外两种方式在全新工作树里跑 `pre-commit`(含 `doc-links`)都**正常通过**,
证明 hooks 经 common dir 共享、门禁不因工作树而失效。
### 10.2 工作树是「提交物自洽性」的照妖镜(2026-10-12 实证)
演练中 `done` 的门禁在干净工作树里报出 2 处失效引用,而**主工作树里同一门禁是绿的**:
```text
docs\内存管理\RAM使用分析与优化方案.md:277 [路径引用] scripts/ram_baseline.tsv
docs\内存管理\RAM使用分析与优化方案.md:585 [路径引用] docs/内存管理/RAM预算台账.md
```
根因:该文档**已提交且无改动**,但它引用的这两个文件**从未 `git add`**(属另一在途任务),
只在作者的主工作树磁盘上存在 → `doc-links` 查的是**文件系统**,于是在主工作树里通过;
一旦换到干净工作树(本演练的 `_probe-base2`),文件不存在 → 必然 FAIL。
**判据与处置**:这不是本 skill 的缺陷,而是被工作树**逼出来的真问题**——
「一个提交是否自洽」不能靠「在我的工作目录里能过」来证明。修复归属**引用方任务**
(补 `git add`,或给引用加 `[计划]` 前缀 / 写明「已删除」状态词,见 `docs/文档规范.md`)。
**副作用提醒**:这同时说明**共享主工作树里的绿门禁不可尽信**——只要有未提交文件躺在磁盘上,
门禁就可能对着「未提交的现状」放行。多任务并行期更该用工作树 + `done` 复核。
### 10.3 `git worktree remove` 中断会留下「裸目录」(2026-10-12 实证)
清理遗留 worktree 时踩到:`git worktree remove --force` 被中断后,`_v2oui-build` 的 `.git` **已被删除**
(于是 `git worktree list` 显示 `prunable`),但目录里**还躺着 2774 个条目**没删完。
**判据**:看到 `prunable` + 目录仍存在 = git 已删掉 gitfile、删除未完成。此时那棵树**已不再是 worktree**
(git 不认它了),`git worktree remove` 无法再用,只能按普通目录删除。
**处置**:`git worktree prune` 清台账 → 手工删目录。删之前必须**重新扫一遍 reparse**——
理由它有 `.git` 时没有联接,不等于现在没有(尤其别让他人趁乱建了联接)。
清除手段用 Python 自底向上删并顺带 `chmod +w`(MDK 产物有 2 个只读文件),
**切忌 `rm -rf`**(MSYS 会递归进联接,实测见 3.3)。
**已知不做**:不引入第二套并行机制(如 git submodule / 多克隆),
保持「同一能力只有一个实现」的收敛原则。
---
## 11. 落地清单与实测记录
| 动作 | 目标 | 状态 |
|---|---|---|
| 新建 | `.dsh/skills/task-worktree/SKILL.md`(含 `name` / `description` / `whenToUse` frontmatter) | ✅ |
| 新建 | `.dsh/skills/task-worktree/scripts/task_worktree.py`(纯标准库) | ✅ |
| 新建 | `.dsh/skills/task-worktree/.gitignore` | ✅ |
| 修改 | `AGENTS.md`「按主题查哪份文档」加一行指向本文档 | ✅ |
| 修改 | `docs/README.md` §9.2 登记本文档 | ✅ |
| 清理 | 移除遗留 worktree `_v1pin-build` / `_v2oui-build` | ✅ 2026-10-12 已清理(脏改动先留档,见 §2) |
| 验证 | §10 的两个待验证项 | ✅ 见 §10 / §10.1 |
### 11.1 端到端实测记录(2026-10-12,隔离集成点演练)
用 `DTU_INTEGRATION_WORKTREE` 把集成点指到一次性工作树、基线用一次性分支,
**全程未触碰真实主线 `v2-on-ui`**:
| 场景 | 结果 |
|---|---|
| `new` 建树 + 双联接(`.venv` / `simulator/SDL2`) | ✅ 透过联接跑解释器得 `Python 3.12.4` |
| 新工作树里跑 `pre-commit`(共享 hooks) | ✅ `doc-links` 等通过;gitlint 也正常拦下非法邮箱 |
| `list` / `status` | ✅ 落后领先、脏文件数、联接清单、锁归属均正确 |
| `lock` acquire / 冲突拒绝 / 幂等 / 错误持有者拒绝 release | ✅ 期望的 rc 全部命中 |
| `done --dry-run` 无冲突 | ✅ 输出预演结果树,零改动 |
| `done --dry-run` 有冲突 | ✅ 拒绝合并(rc=1),提示到任务分支上解冲突 |
| `done` 完整回收(合并 + 审计 + 重生成 + 清场) | ✅ 合并提交生成合规(标题 40 字符 ≤100、正文 ≤80) |
| 清场安全性 | ✅ 联接摘除前后 `.venv` 文件数 2809 → 2809,解释器完好 |
| `abort` 未加 `--force`(有未合并提交) | ✅ 拒绝(rc=1)并列出将丢弃的内容 |
| `abort --force` | ✅ 联接摘除、工作树移除、分支删除、`.venv` 2809 → 2809 |
**实测暴露并已修复的 4 个缺陷**:
1. **锁绑定进程存活 → 形同虚设**:`lock acquire` 是短命进程,一退出锁就被判陈旧、下次必被接管。
改为**时效判定**(当时全局 6h;2026-10-12 起改为按锁分设 TTL,见 §7.1)+ 显式 `--force`,pid 仅作记录。
2. **`git branch -d` 按环境 HEAD 判「已合并」**:集成点被改走时误判、拒绝删除。
改为显式 `merge-base --is-ancestor <branch> <base>` 后再删。
3. **直调 Win32 建联接被拒**(Win32 4392):改走 `mklink /J`(详见 3.3)。
4. **`done` 门禁跑错工作树**(最隐蔽的一个):`run_script` 原用 `REPO_ROOT / script` 拼脚本路径,
而 `REPO_ROOT` 由 `task_worktree.py` 自身位置反推 = **主工作树**。于是 `done` 本意是在集成
工作树里校验,实际却跑了主工作树里的同名脚本;该脚本再按它自己的 `__file__` 反推,又去校验
**主工作树**——校验对象整体错位。
证据(回归前/后对照):直接在工作树里跑 `ci_local.py` 报「扫描 .c: 68 / PASS」,经 `done` 调用
却报「扫描 .c: 71 / FAIL」并附 `[GAP] App/protocol/iec104/*.c | 缺 uvprojx`(那 3 个文件只以
**未跟踪**状态存在于主工作树)。修法 = 从传入的 `cwd`(= 集成工作树)解析脚本路径
(`scripts/ci_local.py` L46 / `scripts/check_registration.py` L52 的 `__file__` 反推是既定约定,
脚本必须由**目标树**里的那一份执行)。修复后同一条命令的
`双端注册反向校验 (uvprojx + CMakeLists)` 由 FAIL 转 **Passed**。
> 缺陷 4 的教训具有普适性:**任何「按 `__file__` 反推仓库根」的项目脚本,都必须由目标工作树里
> 的那一份来执行**,否则门禁会静默地校验另一棵树。本 skill 的 `run_script` 已统一按 `cwd` 解析。
---
## 12. 关联文档
| 主题 | 去处 |
|---|---|
| 提交门禁钩子逐项详解 | `docs/代码规范/pre-commit使用说明.md` |
| 自动化测试与台架流程 | `docs/测试说明/流程与规范/自动化测试流程.md` |
| 文档写作规范(编码/链接/登记) | `docs/文档规范.md` |
| 工具路径环境变量(换机) | `docs/环境与部署/本地工具路径环境变量方案.md` |
| 待办与排序登记簿 | `docs/任务规划/README.md` |