Files
STM32F4-Base/docs/环境与部署/并行任务工作树方案.md
T
wandering1 4a4ad6bc5f chore(全仓): @build/@test 旧文档别名清退为技能调用名
现状口径:/keil-build 与 /serial-boot-test 是 DSH 会话的技能调用名,
不是 shell 命令;「名 + 命令行参数」的命令形态改为可执行体
run_boot_test.py 的真实调用。

- AGENTS.md:别名说明重写为「技能调用名不是 shell 命令」,两条目改
  「编译(/keil-build)」「串口回归(/serial-boot-test)」
- keil-build / serial-boot-test 两 skill 调用名与示例同步;task-worktree
  方案文档去掉 @build/@test 惯例从句;@task 保留(不在点名范围)
- 123 文件现状陈述全改;台架捕获、变更记录/时间线、rename 映射输入
  等 41 行历史证据保留旧名
- gen_memory_svg.py 6 处措辞随改,3 张 SVG 产物同批重跑
- 顺手补并行方案同行漏网计数 51→52 钩子

验证:提交期快钩子对 123 文件实跑全 Passed;check_doc_links --all
rc=0(3038 处、无法判定 0);check_mermaid --all rc=0;YAML 解析 52
钩子不变;skill-size rc=0;py_compile 全过;字节卫生零漂移。
2026-10-30 13:03:12 +08:00

34 KiB
Raw Blame History

并行任务工作树工作流(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 目录布局

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

本仓库 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.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→/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 处失效引用,而主工作树里同一门禁是绿的:

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