Files
STM32F4-Base/docs/环境与部署/并行任务工作树方案.md
T
wandering1 0f7b5a87fb chore(编号): 存量编号全量转语义短语,门禁识别面与语料同批重打
登记簿、文档与代码注释里的存量字母加数字编号(N-x / S-x / T-x / E-x / A-x / 卡号)
一律改写为语义短语,净删 2,028 处引用;删除基线与待确认、并行分道方案、排期详述、
采集保护收尾 4 篇(内容已并入 docs/任务规划/README 与登记簿)。

门禁机器与三张账同批重打:check_ids.py 新增变更记录行豁免与第二根护栏
undecidable_total_ceiling;check_test_traceability.py 只加 stdout/stderr UTF-8
reconfigure(TC_RE 未动);check_cubemx_survival.py 的 --write 固定 newline=LF;
id_registry / id_aliases(+19 条 retired-to-phrase)/ id_known_backlog 重打。

test/ 56 个文件同步 TC 标签语义化,其中 18 个 TU 改了 CHECK / printf 里的标签文字,
是本批唯一改变构建产物的文件集合。

为什么合成一笔、不拆四笔(实测撞闸,非偏好):pre-commit 跑钩子前会 stash 未 staged
部分,拆开后中间提交必然是「新规则 + 旧语料」——实跑可判定 85.15% < 基线 86.00%、
不可判定 1,453 > 上限 1,166,编号门禁直接拦下。编号识别面与它描述的语料只能同码移动。

实跑基线(工作区全量):754 文件 / 8,230 处编号字面量 / 可判定 7,079 = 86.01%
(基线 86.0%,容差 0.1pp)/ 不可判定绝对数 1,151(上限 1,166)/ 校验 1-6 错误级 0 /
告警 = 悬空 2(App/algo/freq.c 的 S2,属 §4.4 同号不同义)+ 孤儿 50。

基线从 84.0 抬到 86.0 的理由见 docs/门禁说明/id-check.md §7.6:本批真删了 2,028 处
引用,可判定面随真实内容上升,不是把分母做小。同批新增排除 docs/硬件平台/ 语料,使
识别面少 264 处——芯片手册位号与网表料号不属编号空间(registry 有 excluded_paths_note)。

⚠ 余量只剩 0.11pp(约 10 处)与绝对数 15 处:把「可判定的引用」改成散文会立刻撞第二
根护栏。另遗留一项:id_aliases 里 retired 的 N / S / T / A 别名没有 scope(U1~U5 有),
今后有人新写一个 A-4,校验 3 不会响,只能靠人,建议下批补 scope。

固件不变证据:115 个 .c/.h 剥注释并归一字符串后与 HEAD 逐字节全等,残留代码差异 0;
App / Src / Drivers 的字符串字面量零改动。@build 0 错误 0 警告;40/40 主机套件、2,889
断言全通过;覆盖率 line 89.2% / func 92.6%;pre-commit 48 钩子 --all-files 全绿;
check_doc_links --all 通过(176 文档,章节引用无法判定 0);逐字节卫生核过:14 个带
BOM 文件与 HEAD 一致、0 个 CR、0 个 U+FFFD,字符串字面量纯 ASCII。

.cubemx_survival_baseline.json 的 watchdog-module 哈希 56bf27 变 2bb051 属合法:
Src/freertos.c 的 USER CODE 区内只有注释文字随编号改写,代码未变。

同批带上体积同码凭证与 RAM 台账行(@build 之后盖戳):RW_IRAM1 79,008 B / 60.3%,
RW_CCM 57,376 B / 87.9%,阈值 92%。
2026-10-26 12:28:14 +08:00

414 lines
31 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 30 钩子 + `@build` + `@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`。
---
## 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` 合入主干 |
| 并发上限 | **同时最多 3 棵工作树** |
**`<ID>` 复用登记簿编号**是为了可追溯:任务分支名直接对应待办条目,回收时销项有据可查。
**并发上限 3** 的依据:`.git` 已 234 MB,每棵工作树要检出约 2400 个受控文件(含 Git LFS 图片),
并发太多磁盘与 I/O 都不划算,人工也管不过来。
---
## 5. 任务生命周期
| 阶段 | 动作 | 并行性 |
| ------ | -------------------------------------------- | ----------- |
| `new` | 建分支 + 工作树 + junction + 台账 | 可并行 |
| 编码 | 在新空间里改代码 | 可并行 |
| 单测产物 | `%TEMP%\dtu_host\<工作树哈希>\` 分家 + 每跑独占 `runs\<pid>-<ns>\` | **不需要锁,但必须按树分家**(旧固定共享名跨树互踩:A 树可能执行 B 树编出的同名 exe,summary 相同不可辨;build/bench 是真独占,继续用锁) |
| 门禁 | `scripts/ci_local.py`(L0 + L0.5,纯软件) | **可并行** |
| 构建 | `@build`(UV4 增量,需持 `build` 锁) | **必须串行** |
| 台架 | `@test` / 板级套件(需先持台架锁,见 §7) | **必须串行** |
| 提交 | 本地 `git commit`(多行正文用 `-F <UTF-8 无 BOM 文件>`) | 可并行 |
| `sync` | `rebase` 追平主干,把冲突控制在任务分支上 | 可并行 |
| `done` | 预演 → 合入主干 → 残留审计 → 重生成产物 → 门禁 → 清理工作树 | **必须串行** |
| `push` | 推送主干/任务分支 | **需用户明确许可** |
**为什么 `done` 必须串行**:合并动作会移动主干引用,两处同时合入会互相覆盖;且主干只在主工作树检出。
---
## 6. 冲突治理
### 6.1 单写者文档
并行失败通常不是代码冲突,而是**多个任务同时改同一个「登记簿」**。以下文件在任务期间
**禁止在任务工作树里修改**,一律由 `done` 回收阶段在主工作树统一处理:
| 文件 | 冲突原因 | 回收时的处理 |
| ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| `docs/任务规划/`(整个文件夹) | 全项目唯一登记簿,每个任务都要销项 | 由主工作树统一改,任务分支不碰 |
| `AGENTS.md`、`docs/README.md`(索引/登记类) | 每个任务都要在「按主题查哪份文档」表、`docs/README.md` §9.2 环境表、变更记录里**各加一行** → 两任务必冲突(2026-10-12 实测:主工作树里本任务的行与他任务的行已并排混在**同一 hunk**) | 由主工作树统一追加;提交时**只 `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` → `@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 台架锁
以下物理资源**各只有一份**,故 `@test` / 板级套件 / 烧录必须串行:
- 串口 `COM6` @115200
- ST-LINK SN `0B250D12A218303030303032`
- 设备 IP `192.168.1.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→`@test`→探针),才配得上 6h。
持有者还可用 `lock acquire ... --ttl <小时>` **自报**更精确的窗口——
`keil_build.py` 即按其 `--timeout` 自报(`timeout/3600 + 15min`,下限 0.5h)。
`done` 前置检查:**陈旧锁自动接管释放**(不再硬失败),仅当锁仍被有效占用时才拒绝回收。
> ⚠ 台架相关的既有陷阱(`--require-selftest-pass` 的陈旧台账、回读校验会复位目标)
> 不因引入工作树而改变,仍按 `docs/测试说明/自动化测试流程.md` 执行。
### 7.2 构建
`@build`(UV4 增量)在新空间里是**独占目录**,但 **UV4 不能两个实例同时 `-b`**(2026-10-12 用户确认,
无需实测)——因此构建**同样纳入串行锁**(`build`),与台架锁并列。
> 结论:任务期真正可并行的只有**编码与静态门禁**(`ci_local.py` 的 L0 / L0.5,纯软件、零硬件);
> `@build` 与烧录/台架都必须排队。
### 7.3 并行道与写范围规则(规划层 ↔ 执行层映射)
规划层(当前任务卡在 `docs/任务规划/UI补齐.md`,后续事项在 `docs/任务规划/登记簿.md`;
五字段 = **DoD / 硬依赖 / 资源 / 写范围 / 状态**)
按**资源属性**把任务切进三条道,本方案(工作树 + 锁)是它的执行层:
| 道 | 资源 | 并行性 | 锁 / 机制 |
|---|---|---|---|
| **台架道** | COM6 / ST-LINK / 设备(§7.1) | **串行**——约束资源,**永不空转**(约束优先,TOC):代码道在跑时必须为下一个台架任务备料 | `bench.lock` |
| **代码道** | CPU / 源码 | **并行**——worktree 独立空间;前提 = 写范围互不相交 + 强门禁(31 钩子 `ci_local`,同 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`(沿用本仓库 `@build` / `@test` 的 `@` 惯例)
**执行体**:`.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`(L0 + L0.5,可并行) | 每个任务各自跑 |
| 任务期 | `@build`(0 错 0 警,**持 `build` 锁串行**) | 每个任务各自跑 |
| **新空间首次** | `@build` 一次,生成 `.map` | 见下表下注释:不先构建,体积类门禁必然红 |
| 任务期(可选) | `@test` / 板级套件(持 `bench` 锁) | 需真机验证的任务 |
| 回收期 | 重生成产物 → `ci_local.py` → `@build` | `done` 内强制(`--skip-gates` 可跳过) |
| 人工 | push 许可 | 回收后 |
> ⚠ **新开工作树后先 `@build` 一次再谈门禁**(实测 2026-10-25):`.map` 是每棵树各自的构建产物、
> 不入版本库,所以依赖它的事实源的门禁——`mem-numbers`、`mem-budget`、以及 `ci_local.py` 的
> `L0c`——在从未构建过的新空间里**必然红**。这不是代码缺陷,脚本此时也会把这句原因打出来
> (`check_mem_numbers.py` / `ram_baseline.py` 的提示文案)。顺带一条同源提醒:`done` 的门禁
> 排在 `@build` 之前(见下文陷阱条目),同一族误报在回收期也会重现一次。
**`done` 的完成判据**:主干上 `git merge --no-ff` 成功 + `ci_local.py` 全绿 + `@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. 关联文档
| 主题 | 去处 |
|---|---|
| 提交门禁 40 个钩子 | `docs/代码规范/pre-commit使用说明.md` |
| 自动化测试与台架流程 | `docs/测试说明/自动化测试流程.md` |
| 文档写作规范(编码/链接/登记) | `docs/文档规范.md` |
| 工具路径环境变量(换机) | `docs/环境与部署/本地工具路径环境变量方案.md` |
| 待办与排序登记簿 | `docs/任务规划/README.md` |