# 并行任务工作树工作流(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/.json # 任务台账:分支 / 联接 / 门禁与台架戳 ├── .state/locks/ # 独占锁:build.lock(UV4)+ bench.lock(COM6/ST-LINK/设备 IP) └── -/ # ← 一棵工作树(分支 task/-) ├── .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/-/` 里的那几个路径都不在 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/-` | `worktree/` | 两条**并发红线由本方案单方守住**(插件不感知):`build`(UV4 单实例)与 `bench`(COM6 / ST-LINK / 设备 IP 唯一)这两把锁的位置与 TTL 口径见 §7.1 / §7.2——插件树里跑 `/keil-build` 时,`keil_build.py` 会照旧自持自释,持有者名从 cwd 反推为 `pw:`;`bench` 仍须手工占同一个名。共享联接(§3.3) 插件不会建,由本 skill 的 `plugin adopt ` 补(执行体命令行见 §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/-`,从主干当前 HEAD 切出 | | `` | **沿用 `docs/任务规划/README.md` 的编号空间**(`P0-x`/`N-x`/`S-x`/`T-x`/`B-x`/`C-x`/`D-x`);无对应条目的临时任务用 `X-<日期>-` | | 合并方式 | `git merge --no-ff` 合入主干 | | 并发上限 | **同时最多 8 棵登记工作树**(2026-10-29 由 3 上调,用户裁定;另有插件树不计入,见 §3.4) | **`` 复用登记簿编号**是为了可追溯:任务分支名直接对应待办条目,回收时销项有据可查。 **并发上限的依据**(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\-\` | **不需要锁,但必须按树分家**(旧固定共享名跨树互踩:A 树可能执行 B 树编出的同名 exe,summary 相同不可辨;build/bench 是真独占,继续用锁) | | 门禁 | `scripts/ci_local.py`(静态门禁 + 主机侧纯逻辑单测,纯软件) | **可并行** | | 构建 | `/keil-build`(UV4 增量,需持 `build` 锁) | **必须串行** | | 台架 | `/serial-boot-test` / 板级套件(需先持台架锁,见 §7) | **必须串行** | | 提交 | 本地 `git commit`(多行正文用 `-F `) | 可并行 | | `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 自动改写 | 手工并 `` 条目 → 重跑 `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 :` 逐项 `grep -F`), **不要用 `tail -N` 目测**(会漏看被合并在中间的行),也**不要把合并前就存在的历史重复算进来**。 ### 6.3 冲突时的解决位置 `done` 先做**非破坏性预演**:`git merge-tree --write-tree v2-on-ui task/-`。 - `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 [--base B] [--lean] [--needs-bench]` | 建分支 `task/-` + `_wt/-/` 工作树 + 共享联接 + 台账 | | `list` | 所有任务一览:分支 / 落后领先 / 脏文件数 / 台架状态 + 两个锁的归属 | | `status ` | 单任务详情:未提交清单、相对基线漂移、台架记录、已建联接 | | `sync ` | `rebase` 追平主干,把冲突控制在任务分支上 | | `lock acquire\|release\|status ` | 独占资源锁(`--task` / `--wait` / `--force` / `--bench-pass`) | | `done [--dry-run] [--skip-build] [--skip-gates]` | 预演 → 合入主干 → 残留审计 → 重生成产物 → 门禁 → 清场 | | `abort [--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 ` 后再删。 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` |