chore(gate): 门禁钩子改走 gate_python 启动器,worktree 统一插件模型

门禁解释器解耦
  44 条 language: system 钩子此前把解释器写成仓库检出的绝对路径,换机、
  换克隆目录都要重写。改为 entry 只写 scripts/gate_python.py + 脚本名,
  由启动器按候选链解析:$DTU_GATE_PYTHON > <仓库>/.venv >
  <仓库>/.gate/python > PATH。启动器用 execve 替换自身,并把选中的
  解释器经 DTU_GATE_PYTHON 传给子进程,保证脚本内再调 Python 时一致。
  实测 pre-commit 4.6.2 不展开 ${VAR};language: script 的 shebang 只做
  纯 PATH 改写,会把决定权交给 PATH 首个 python,故不采用。
  ci_local.py 预检识别启动器并报出解析结果。

并行工作树统一
  task-worktree 新增 bench-gate(强制台架验收)与 merge(编排式合并),
  删除 new/abort,done 标 DEPRECATED;台架通过记录改存
  .agents/worktrees/.bench-pass-log。keil_build.py 的 plugin_tree_owner
  识别 .agents/worktrees/<name> 并返回 pw:<name>。
  check_doc_links / gen_refgraph / check_layer_deps 的扫描排除面补上
  .agents;并行任务工作树方案重写为单一模型(7 节),取消工作树数量上限,
  删历史实测记录节,两处 §7.3 悬空引用改为按语义引用。

文档与约定
  本地工具路径环境变量方案 §5 重写为四节:谁跑在哪个 Python 上、写新钩子、
  预检对号入座、动环境五步。文档规范的变更记录例外名单由 5 篇减为 4 篇
  (本篇实际没有该节)。AGENTS.md 补委派验收口径三条。

验证:pre_commit run --all-files 48 钩子全 Passed;ci_local.py
--preflight-only 报 44 条经启动器 -> 仓库 .venv;keil-build 0 errors
0 warnings;check_doc_links --sections-strict 全通过。
This commit is contained in:
2026-11-15 19:39:56 +08:00
parent 9f68b7bc03
commit 96a3d15efc
17 changed files with 826 additions and 720 deletions
+1 -1
View File
@@ -258,7 +258,7 @@ docs/
| 文档 | 内容 |
|---|---|
| [`项目部署环境要求.md`](环境与部署/项目部署环境要求.md) | 开发机软件环境、目标板资源预算、现场部署与验收步骤 |
| [`并行任务工作树方案.md`](环境与部署/并行任务工作树方案.md) | **多任务并行开发机制**:`git worktree` 隔离、目录与共享/独占分配(junction 规则)、`task-worktree` SKILL 命令面(`@task` 仅为文档别名,显式调用 `/task-worktree`)、单写者文档与合并残留审计、台架锁;**已落地并端到端实测**(见其 §11) |
| [`并行任务工作树方案.md`](环境与部署/并行任务工作树方案.md) | **多任务并行开发机制(单一模型)**:DSH 插件创建工作树,`task-worktree` 补共享环境、构建/台架锁、`bench-gate` 台架验收门禁与 `merge` 编排式合并;含目录联接、冲突规则与容量建议 |
| [`子Agent角色自动注入实现说明.md`](环境与部署/子Agent角色自动注入实现说明.md) | **`spawn_teammate` 角色 persona 自动注入的实现参考 + 排障参照**(宿主 0.2.2-rc.1 + 2026-11-13 台架实证快照):事件面事实(两参回调/子会话时序/lead 侧接力)、工具白名单降级、兜底与去重、失败模式史、部署生效链(pnpm file: 拷贝陷阱)、诊断日志排障速查;使用侧在 `.dsh/roles/README.md` |
| [`CI/README.md`](CI/README.md) | **远程 CI(Gitea Actions)唯一入口**:服务端开关 / runner 部署与保活 / workflow / 排障。**只涉及本机一台 runner `win-stm32-ci`,其他机器与项目都不涉及**;日常改代码用本地门禁 `scripts/ci_local.py`,不必读这里 |
+1 -1
View File
@@ -35,7 +35,7 @@
## 2. 全项目推进顺序
> 本节是全项目唯一的顺序权威。模块文档只写局部设计与重开条件,不另排一套路线图。
> 写范围相交怎么排队、构建与台架怎么串行 → `docs/环境与部署/并行任务工作树方案.md` §7.3。
> 写范围相交怎么排队、构建与台架怎么串行 → `docs/环境与部署/并行任务工作树方案.md` 的「冲突与单写者规则」与「从创建到交付的完整流程」两节。
### 2.0 顺序表
@@ -353,7 +353,7 @@ flowchart TD
PAGE -. 二期另立项 .-> NEXT[4 kHz 全速 / 突变量启动 / COMTRADE / 104 文件传输];
```
图内不写步骤号,节点名与 §10 各行语义逐字可对。并行度规则见 `docs/环境与部署/并行任务工作树方案.md` §7.3;**UI 道是单写者**——菜单树与中文字库都是整文件生成物,动叶子的那条道不许与别的改 UI 的道并行。
图内不写步骤号,节点名与 §10 各行语义逐字可对。并行度规则见 `docs/环境与部署/并行任务工作树方案.md` 「冲突与单写者规则」一节;**UI 道是单写者**——菜单树与中文字库都是整文件生成物,动叶子的那条道不许与别的改 UI 的道并行。
### 板级远程半轮记录(2026-10-31,无注入·无人值守)
+2 -2
View File
@@ -185,7 +185,7 @@
> 同批实测的另两条工具侧事实:新路径**以 `/` 结尾**才是"挪进该目录并保留文件名"(见上例注释);`--apply` 收尾会自动跑
> `check_doc_links.py --all`,在带 junction 联接(`.venv`、`simulator/SDL2`)的任务工作树里会把联接目录扫成**假红**,
> 所以树内批量搬迁要加 `--no-verify`,全库 `--all` 复核留到主工作树跑(→ `docs/环境与部署/并行任务工作树方案.md`)。
> 所以树内批量搬迁要加 `--no-verify`,全库 `--all` 复核留到主工作树跑(工作树分工与目录边界见 `docs/环境与部署/并行任务工作树方案.md`)。
> 另:脚本结尾的 `ℹ` 提示在 GBK 代码页会抛 `UnicodeEncodeError` 让进程**以退出码 1 收场**(此时 `git mv` 与改写**已经落盘**,
> RC 与真实结果相反)→ 2026-10-30 已在 `scripts/rename_ref.py` 内把 stdout/stderr `reconfigure(encoding="utf-8")`,
> 与 `check_ids.py` / `check_doc_links.py` / `check_test_traceability.py` 同法。
@@ -348,7 +348,7 @@
| **一级标题** | 文档首行为 `# 标题`,标题即主题,不写"文档""说明"等冗余后缀 |
| **开篇引言** | 可选 `>` 引用块,写明**适用对象 / 阅读前提 / 相关文档** |
| **章节编号** | 多章节文档用 `## 1.` `### 1.1` 编号,便于交叉引用(`§3.2`) |
| **变更记录** | **一律不设该节**(用户裁定 2026-10-28 起新文档、2026-10-29 起老文档也清完;标题变体 `修订记录` / 带编号的 `## 6. 变更记录` / `## 九、变更记录` 同规则)——文档只写现状、改了什么是正文说了算,改动过程查 `git log`。**例外只有 5 篇**(整表保留:表里是真台账、且被别处当唯一证据出处引用):`docs/子系统设计/看门狗设计说明.md`、`docs/子系统设计/配置文件设计说明.md`、`docs/内存管理/RAM预算台账.md`、`docs/UI设计/内容页架构.md`、`docs/环境与部署/本地工具路径环境变量方案.md`;`scripts/archive/` 下的归档文档整片都是历史,不受本条约束 |
| **变更记录** | **一律不设该节**(用户裁定 2026-10-28 起新文档、2026-10-29 起老文档也清完;标题变体 `修订记录` / 带编号的 `## 6. 变更记录` / `## 九、变更记录` 同规则)——文档只写现状、改了什么是正文说了算,改动过程查 `git log`。**例外只有 4 篇**(整表保留:表里是真台账、且被别处当唯一证据出处引用):`docs/子系统设计/看门狗设计说明.md`、`docs/子系统设计/配置文件设计说明.md`、`docs/内存管理/RAM预算台账.md`、`docs/UI设计/内容页架构.md`;`scripts/archive/` 下的归档文档整片都是历史,不受本条约束 |
> 数"还剩几篇留着这节"的命令(**别只 grep 裸标题**——带编号的 `## 6. 变更记录`、中文序号的 `## 九、变更记录`、`## 修订记录` 三种写法都要算):
> `grep -rn "^## " docs scripts` 再看命中的标题;2026-10-29 实测 **5 篇**真台账(上表那五篇)+ `scripts/archive/` 归档一篇。
@@ -1,442 +1,187 @@
# 并行任务工作树工作流(worktree + `task-worktree` skill)
# 并行任务工作树方案
> **这个文件是什么**:多任务并行开发的**机制规格**——如何为每个任务开一个互不干扰的工作空间、
> 干完如何合并回主干、哪些资源必须串行、冲突怎么治理。
> **你要用它做什么**:把互不相干的开发任务放进独立目录,避免同时改代码、跑构建和上台架时互相干扰。
>
> **状态**:**已落地并端到端实测**(2026-10-12)——skill 在 `.dsh/skills/task-worktree/`,
> 实测记录与暴露并修复的缺陷见 §11。
> **现行做法**:DSH 的 worktree 插件创建并进入工作树;本项目的 `task-worktree` skill
> 负责共享环境、硬件与构建排队、台架验收门禁及合并收尾。插件简化的是「开空间」,
> 不是「自动验证并合入主干」。
>
> **决策基线**(2026-10-12 用户确认):工作树放**仓库同级 `_wt/`**;集成分支**沿用 `v2-on-ui`**;
> `done` 回收**不自动 push**(沿用「push 需用户明确许可」)。
> **现行基线**(2026-11-15):主干为 `master`;本机插件为
> `@deepseek-ai/dsh-experimental-tool-worktree` 0.2.2-rc.1,模型工具为
> `create_worktree`。本文规定本仓采用的分工;插件自身行为以其安装版本的说明和源码为准。
---
## 1. 目的与范围
## 1. 单一模型:插件树 + 项目验证层
**要解决的问题**:多个任务同时推进时,若共用同一个工作目录,会互相踩——构建产物、日志目录、
Python 环境、以及 `docs/任务规划/` 这类**全局唯一顺序权威**都会被并发写坏。
所有任务统一走插件树生命周期,不再并存登记任务链。
**做法**:每个任务一棵 `git worktree` + 一条任务分支,任务之间物理隔离;干完由固定命令合回主干。
| 工作 | 入口 | 为什么 |
|---|---|---|
| 创建分支与工作树、切换当前会话目录 | DSH `create_worktree` | 一次调用完成,直接返回路径、分支和固定基线提交 |
| 接入共享 Python 环境与模拟器 SDK | `task-worktree plugin adopt` | 插件不复制本机未入库的依赖目录 |
| 查看工作树与共享联接状态 | `task-worktree plugin list` | 不依赖插件私有状态文件 |
| Keil 构建排队 | `/keil-build` 自动持有并释放 `build` 锁 | UV4 构建仍须串行 |
| 烧录与台架排队 | `task-worktree lock acquire/release bench` | 串口、ST-LINK 和设备各只有一份 |
| 台架验收门禁 | `task-worktree bench-gate <branch>` | 检查该分支是否有 `bench-pass` 记录,合并前调用 |
| 编排式合并(预演→合并→重生成→门禁→清场) | `task-worktree merge <branch>` | 复用原 `done` 逻辑,数据源改为 git metadata |
| 树内编码、静态检查与主机侧单测 | 各任务自己的目录 | 文件与构建产物互不覆盖;共享资源仍按锁串行 |
| 推送远端 | 用户明确许可后另行执行 | 任何本地收尾都不等于获得推送许可 |
**范围**:只解决「开发期隔离 + 回收期合并」的机械流程。**不改变**既有的分支纪律
(push 需许可)、门禁口径(pre-commit 52 钩子 + `/keil-build` + `/serial-boot-test`)与文档约定。
**`task-worktree` 没有重造 Git 工作树。** 底层隔离仍由 Git 提供;它补足的是本仓特有、插件不承担的约束。
---
## 2. 创建与接入
## 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 目录布局
### 2.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/… # 本任务独占的构建产物
create_worktree({ name: "fix-serial-reconnect", from: "master" })
```
**工作树绝不能建在仓库内部**(`STM32F4-Base/` 里):那会变成未跟踪目录,污染 `git status`、
`pre-commit --all-files` 与全库搜索。故统一放仓库同级的 `_wt/`。
- `name` 同时作为新分支名和目录名,使用描述任务的短横线语义名;不要省略成难辨认的自动随机名。
- `from` 必须是本地已有的提交、分支或标签;插件不会替你 `fetch`。通常显式写 `master`,避免从别的任务树继续叠基线。
- 成功后保存返回的 `path`、`branch`、`baseCommit` 和 `repositoryRoot`。会话目录已经切到返回的 `path`,后续编辑与测试都在那里进行。
- 暂存、未暂存、未跟踪及被忽略的源文件**不会**带进新树。若任务依赖主树未提交内容,先明确其归属与提交安排,不要假设插件会复制。
- 离开只表示切换会话目录,**不删除目录和分支**;本版工具不负责合并或最终回收。
### 3.2 共享 vs 独占
### 2.2 补共享联接
| 资源 | 归属 | 原因 |
| ------------------------------------------------------------------ | ---------------- | ------------------------------------------------------------- |
| `.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 |
插件只检出受控文件,因此新树没有本机的 `.venv/` 与 `simulator/SDL2/`。在主树执行:
### 3.3 junction 规则(**不能用 symlink**)
```bash
PY=".venv/Scripts/python.exe"; TW=".dsh/skills/task-worktree/scripts/task_worktree.py"
"$PY" "$TW" plugin list
"$PY" "$TW" plugin adopt fix-serial-reconnect
"$PY" "$TW" lock status
```
本仓库 `core.symlinks=false`:git 里的符号链接会被检出成「内容是目标路径的普通文本文件」,
不是链接。因此**共享 `.venv` 只能靠 Windows 目录联接(junction)**,且 junction 必须创建在
**版本控制之外**(`_wt/<ID>-<slug>/` 里的那几个路径都不在 git 索引内,安全)。
`plugin adopt` 只补缺失的目录联接,不覆盖已有实体目录,不修改任务源码。命令返回后必须确认两项都不再缺失。
创建方式用 `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>` |
| `simulator/SDL2` | 构建模拟器的硬依赖 | 模拟器配置直接失败;该路径由构建文件固定,没有环境变量替代入口 |
| `.venv` | 有替代入口,但默认应共享 | 主机侧检查可能回退到机器级 Python,缺依赖后产生与代码无关的门禁失败 |
两条**并发红线由本方案单方守住**(插件不感知):`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)。
目录联接只共享这些与任务无关的大目录。源码、Git 索引、Keil/模拟器构建产物、日志和 `ci-artifacts/` 仍归各树独占。
`.workbuddy-ai/` 与 `.mnemon/` 不做跨树共享,避免并发写同一份本机状态。
**为什么允许它在仓内破例**(与 §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 节(本文只记边界,不重述步骤)。
清场前必须先摘联接,再处理工作树目录;否则目录删除工具可能沿联接进入主树,误删共享环境。手工摘除只可用
`os.rmdir` 这类「只删联接本身」的操作,禁止对联接目录做递归删除。
---
## 4. 分支模型
| 项 | 取值 |
| -------- | ---------------------------------------------------------------------------------------------------- |
| 集成分支(主干) | `v2-on-ui` |
| 任务分支 | `task/<ID>-<slug>`,从主干当前 HEAD 切出 |
| `<ID>` | **语义短语**(如 `wave-replay-fix`),对应 `docs/任务规划/README.md` §2 顺序表里的那一行;纯临时任务用 `<日期>-<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/任务规划/README.md` §2,设计类待办在各模块文档);
五字段 = **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. **里程碑不拖延、功能顺延下一轮**(时间驱动):里程碑的退出判据写在任务所属文档,判据不满足则该**功能**
移入下一波次,里程碑本身按判据改期而非无限等待。
> 规划层唯一权威仍是 `docs/任务规划/README.md` §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 处失效引用,而**主工作树里同一门禁是绿的**:
## 3. 目录与分支
```text
docs\内存管理\RAM使用分析与优化方案.md:277 [路径引用] scripts/ram_baseline.tsv
docs\内存管理\RAM使用分析与优化方案.md:585 [路径引用] docs/内存管理/RAM预算台账.md
STM32F4-Base/ # 主工作树,master 的唯一集成点
├── .agents/worktrees/<name>/ # 插件树,目录内 .gitignore 自我忽略
├── .dsh-worktrees/worktree/<name>/ # 旧插件遗留位置;存在时继续受桥接与扫描排除保护
├── .venv/ # 唯一 Python 实体环境
├── simulator/SDL2/ # 唯一模拟器 SDK 实体
└── MDK-ARM/ # 主树自己的构建产物
```
根因:该文档**已提交且无改动**,但它引用的这两个文件**从未 `git add`**(属另一在途任务),
只在作者的主工作树磁盘上存在 → `doc-links` 查的是**文件系统**,于是在主工作树里通过;
一旦换到干净工作树(本演练的 `_probe-base2`),文件不存在 → 必然 FAIL。
| 项 | 说明 |
|---|---|
| 默认位置 | 仓内 `.agents/worktrees/` |
| 分支 | 与 `name` 同名 |
| 基线 | 调用时固定到本地提交 |
| 共享联接 | `plugin adopt` 补建 |
| 台架验收记录 | git notes / reflog,由 `bench-gate` 读取 |
| 合并 | `task-worktree merge <branch>` 编排 |
| 活跃数量 | 不设上限;实际受磁盘、构建排队、台架时间与人的跟踪精力约束 |
**判据与处置**:这不是本 skill 的缺陷,而是被工作树**逼出来的真问题**——
「一个提交是否自洽」不能靠「在我的工作目录里能过」来证明。修复归属**引用方任务**
(补 `git add`,或给引用加 `[计划]` 前缀 / 写明「已删除」状态词,见 `docs/文档规范.md`)。
插件树位于仓内是受控例外,不代表可以任意在仓库里建副本。其目录池由插件写入自己的忽略文件;文档链接、引用关系图等全库扫描还必须排除这些目录,避免把整份仓库副本当成新内容。新增或升级插件后,要复核实际目录与扫描排除面是否仍一致。
**副作用提醒**:这同时说明**共享主工作树里的绿门禁不可尽信**——只要有未提交文件躺在磁盘上,
门禁就可能对着「未提交的现状」放行。多任务并行期更该用工作树 + `done` 复核。
## 4. 从创建到交付的完整流程
### 10.3 `git worktree remove` 中断会留下「裸目录」(2026-10-12 实证)
### 4.1 创建与开发
清理遗留 worktree 时踩到:`git worktree remove --force` 被中断后,`_v2oui-build` 的 `.git` **已被删除**
(于是 `git worktree list` 显示 `prunable`),但目录里**还躺着 2774 个条目**没删完。
1. 确认任务基线是 `master` 的本地提交,并为任务写语义名。
2. 用 `create_worktree` 创建并进入。
3. 补两项共享联接;新树按需先建 `ci-artifacts/`,一次性输出都放这里。
4. 只在任务树中编辑。提交前完成该任务应有的文档检查、主机侧单测、`/keil-build` 和台架验证。
5. 提交仍须用户许可。属主完成应交付的提交后,才允许进入合并。
**判据**:看到 `prunable` + 目录仍存在 = git 已删掉 gitfile、删除未完成。此时那棵树**已不再是 worktree**
(git 不认它了),`git worktree remove` 无法再用,只能按普通目录删除。
### 4.2 共享资源排队
**处置**:`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 |
| 编码、彼此独立的静态检查、主机侧单测 | 可并行 | 主机测试临时目录必须按工作树分家,不能共用固定构建目录 |
| `/keil-build` | 串行 | `keil_build.py` 自动占放 `build` 锁,调用者不要再手工包一层 |
| 烧录、串口启动回归、板级套件 | 串行 | 先占 `bench`,结束后用 `--bench-pass` 释放以记录验收 |
| 合并回 `master` | 串行且只在主树 | 主干引用和主树构建产物都是单点 |
| `push` | 不在自动流程内 | 必须另获用户许可 |
### 11.1 端到端实测记录(2026-10-12,隔离集成点演练)
插件树的锁持有者写作 `pw:<name>`。上台架前后使用同一个名字:
用 `DTU_INTEGRATION_WORKTREE` 把集成点指到一次性工作树、基线用一次性分支,
**全程未触碰真实主线 `v2-on-ui`**:
```bash
"$PY" "$TW" lock acquire bench --task pw:fix-serial-reconnect --note "串口重连回归"
# 烧录与台架验证
"$PY" "$TW" lock release bench --task pw:fix-serial-reconnect --bench-pass
```
| 场景 | 结果 |
锁按有效期兜底,不以占锁命令的进程是否存活判定。不要用删锁文件替代正常释放;不要强行接管仍被他人有效占用的锁。
### 4.3 台架验收门禁
需要真机验收的任务,在合并前运行:
```bash
"$PY" "$TW" bench-gate fix-serial-reconnect
```
该命令检查分支 `fix-serial-reconnect` 是否有对应的 `bench-pass` 记录(由 `lock release --bench-pass` 写入)。无记录则拒绝合并并提示补做台架验证。纯文档、纯主机侧测试等不需要台架验收的任务可跳过此步。
### 4.4 编排式合并
```bash
"$PY" "$TW" merge fix-serial-reconnect
```
该命令在主树执行完整收尾编排:非破坏性预演 → `git merge --no-ff` → 重生成受影响的生成物 → 全量门禁 → 安全清场(摘联接、删目录、删分支)。有冲突时中止并提示回任务分支解决,不在主树随手改造成另一份实现。
也可手动分步执行:先 `bench-gate`,再 `git merge --no-ff`,再跑门禁与清场。编排式合并只是把这些步骤串起来,不引入额外状态。
**合并落主干不等于验证结束。** 合并提交期的钩子可能只检查冲突文件;合并后仍要在主树跑全量静态检查、主机侧单测与 `/keil-build`,并重算确实受影响的生成物或基线。
### 4.5 回收
1. 确认任务分支已合并,或用户明确决定放弃;未提交与未跟踪内容必须逐项判定,不能只看最后一次提交。
2. 先 `plugin unadopt <name>` 摘共享联接;再按 `task-worktree` 的「非登记工作树安全回收」规程处理目录与分支。
3. 最后复核 `git worktree list` 和 `lock status`;以主干提交记录证明完成,不以目录消失代替合并证明。
## 5. 冲突与单写者规则
工作树隔离的是目录,不会消除两个任务改同一文件时的语义冲突。任务开始前先划分写范围;相交时明确谁先合,后合者追平主干并重新验证。
| 文件或目录 | 任务期规则 | 合并后处理 |
|---|---|---|
| `docs/任务规划/` | 默认只由主树写;任务树不并发销项 | 主树统一更新,完成即删对应待办行 |
| `AGENTS.md`、`docs/README.md` 的索引区 | 避免多个任务同时追加同一索引块 | 主树统一登记;只提交本任务应带的改动 |
| FreeRTOS 任务表、内存快照与体积凭证 | 不手工拼合两份生成结果 | 基于合并后的真实源码和最新构建重生成 |
| `MDK-ARM/*.uvprojx` | 新源文件按双端注册规程改;UV4 自动抖动不入库 | 检查注册并重新构建 |
| `.pre-commit-config.yaml` | 新增钩子或检查范围时声明合并次序 | 合并后跑全量门禁,不能只信任务树结果 |
双方都改忽略规则或清单时,还要做三方残留审计:去掉失效规则与本次引入的重复项,不把历史既有重复误算成本次合并结果。
## 6. 容量与验证边界
活跃工作树不设数量上限。实际约束是磁盘、构建排队、台架时间与人的跟踪精力——这些才是真上限,数字只是表象。
每棵树的源码与构建产物独立;`.venv` 和 SDL2 共享。Git LFS 的实际文件不会因为「属于同一仓库」就自动出现在插件新树中:现役插件检出时跳过过滤器,可能留下指针对象。需要读取参考图或执行依赖真实二进制内容的检查时,先确认该树拿到的是实际内容,不要把「检出成功」当成「依赖齐全」。
新树首次运行内存与体积门禁前必须先构建,因为映射文件不是受控文件。树内全库扫描若误穿共享联接或仓内工作树副本,先修扫描边界并区分真问题,不能靠忽略输出收口。
## 7. 权威分工与外部参考
| 内容 | 唯一操作入口 |
|---|---|
| `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 |
| 锁、bench-gate、merge、非登记树安全回收 | `/task-worktree` 及其 `reference/` 深规程 |
| Keil 构建与 `build` 锁细节 | `/keil-build` |
| 烧录与串口启动回归 | `/stlink-flash`、`/serial-boot-test` |
| 文档检查与提交规范 | `docs/文档规范.md`、`docs/代码规范/pre-commit使用说明.md` |
| 任务推进顺序 | `docs/任务规划/README.md` |
**实测暴露并已修复的 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` |
- [DSH 工具目录:create_worktree](https://deepseek-harness.github.io/deepseek-harness/en/reference/tool-catalog)
- 本机包说明:`@deepseek-ai/dsh-experimental-tool-worktree` 与 `@deepseek-ai/dsh-experimental-worktree` 0.2.2-rc.1
@@ -1,7 +1,8 @@
# 本地工具路径环境变量方案
> 本文档描述项目的本机工具路径环境变量机制:变量清单、解析与失败语义、各级配置方法、验收判据
> (2026-10-02 实施并生效;实施过程与后续变更见文末变更记录)。
> (2026-10-02 实施并生效;2026-11-15 按当前代码实况同步:补齐控制类变量清单、修正 UV4/ST-LINK
> 解析链、更新钩子计数)。
> **约束(现行)**:只用进程/用户/CI 服务环境变量配置本机差异;不提交本机路径,不引入 `.env`/`python-dotenv`,
> 不把个人工具路径写入共享 workflow;仓库内部路径继续相对脚本/仓库根解析。
@@ -9,8 +10,8 @@
| 变量 | 必需性 | 值的形态 | 使用者 | 默认/回退 |
|---|---|---|---|---|
| `DTU_KEIL_UV4` | MCU 构建机必需 | UV4.exe 完整路径,例如 `D:\Tools\Keil\UV4\UV4.exe` | `keil_build.py`(`/keil-build`) | 不设即报错(exit 2);不从 C 盘旧路径猜测 |
| `DTU_STLINK_CLI` | 复位/烧录/回读机必需 | `ST-LINK_CLI.exe` 完整路径,例如 `C:\Program Files (x86)\STMicroelectronics\STM32 ST-LINK Utility\ST-LINK Utility\ST-LINK_CLI.exe` | `stlink_flash.py` | 未设时回退 PATH(`where ST-LINK_CLI.exe` 首个命中);已设但无效 = 硬报错 |
| `DTU_KEIL_UV4` | MCU 构建机必需 | UV4.exe 完整路径,例如 `D:\Tools\Keil\UV4\UV4.exe` | `keil_build.py`(`/keil-build`) | 解析链:`--uv4` > 项目档案 `.serialtest/config.json` 的 `uv4` > `$DSH_KEIL_UV4` > `$DTU_KEIL_UV4` > PATH;全部落空即报错(exit 2),不从 C 盘旧路径猜测 |
| `DTU_STLINK_CLI` | 复位/烧录/回读机必需 | `ST-LINK_CLI.exe` 完整路径,例如 `C:\Program Files (x86)\STMicroelectronics\STM32 ST-LINK Utility\ST-LINK Utility\ST-LINK_CLI.exe` | `stlink_flash.py` | 解析链:`--cli` > 档案 `stlink_cli` > `$DSH_STLINK_CLI` > `$DTU_STLINK_CLI` > PATH;已设但无效 = 硬报错 |
| `DTU_PRECOMMIT_PYTHON` | 可选(覆盖用) | 含 `pre_commit` 模块的 `python.exe` 完整路径 | `ci_local.py` 静态门禁、CI `static-gates` | 未设时按 §1.1 探测(当前解释器 → `.venv` → PATH);已设但无效 = 硬报错 |
| `DTU_GRAPHVIZ_DOT` | 可选(覆盖用) | `dot.exe` 完整路径 | `gen_refgraph.py` | 未设时只查 PATH;无 Graphviz 时降级为"生成 .dot/.json、跳过渲染" |
| `DTU_MSVC_CL` | 可选(覆盖用) | x64 `cl.exe` 完整路径(`bin\Hostx64\x64` 下) | `unit_test_harness.py` 主机侧纯逻辑单测、CI `host-unit-tests` | 未设时按 VS 安装根自动探测(2019/2022/2026 各 edition);已设但不符 `bin\Hostx64\x64\cl.exe` = 硬报错 |
@@ -18,6 +19,26 @@
| `DTU_VSWHERE` | 可选(覆盖用) | `vswhere.exe` 完整路径 | `build_sim.ps1` | 未设时用 `%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe` 推导;已设但无效 = 硬报错 |
| `DTU_CPPCHECK` | 可选(覆盖用) | `cppcheck.exe` 完整路径 | CI `static-gates` cppcheck 步骤 | 未设时用 PATH(`Get-Command`);已设但无效 = 硬报错。本地 `ci_local.py` 静态门禁不读该变量:cppcheck 钩子按 pre-commit `entry: cppcheck` 从 PATH 解析 |
### 1.2 控制与辅助变量(非「本机工具地址」,但同族登记)
这些变量不指向可执行文件,而是给脚本传控制参数或路径覆盖;同样只在本机/CI 环境设置,不入库。
| 变量 | 使用者 | 语义 |
|---|---|---|
| `DTU_LOCK_HELD` | `keil_build.py` | 调用方已持 build/bench 锁时设它(值如 `build`),脚本跳过自持自释,避免自我死锁 |
| `DTU_TASK_ID` | `keil_build.py` | 显式声明 build 锁持有者名(原登记任务链的遗留入口,插件树时代一般不用) |
| `DTU_GEN_PYTHON` | `check_generated_fresh.py` | 生成器重跑用的解释器;优先级:显式变量 > 当前解释器 > 仓库 `.venv` > PATH,且必须探针校验 |
| `DTU_HOST_BUILD` / `DTU_HOST_BUILD_KEEP` | `unit_test_harness.py` | 主机单测构建目录覆盖 / 保留调试产物(默认每次随机临时目录) |
| `DTU_MS_COVERAGE` | `check_test_coverage.py` | `mscoverage.exe` 完整路径(设了就必须有效,不静默回退) |
| `DTU_REPO_ROOT` | `board_ui_nav.py` | 仓库根覆盖,便于从别的目录或工作树跑同一份取证脚本 |
| `DTU_WT_ROOT` / `DTU_TASK_BASE` / `DTU_INTEGRATION_WORKTREE` | `task_worktree.py` | 工作树根目录 / 默认基线分支 / 集成点覆盖(隔离演练用);随登记任务链退役仅剩兼容读取 |
| `DSH_SERIAL_CONFIG` | `keil_build.py`(经 serial-assistant) | 串口助手配置档案位置 |
| `DSH_UINAV_WALK` | `board_ui_nav.py` | UI 走查日志目录覆盖 |
| `DTU_BENCH` | (编译宏,非环境变量) | 板级 bench 固件的编译期开关;列在此处只为与同名易混的环境量区分——它**不是** shell 变量 |
> CI workflow 内部还会向 run 块注入 `DTU_BASE_SHA` / `DTU_PR_BASE_SHA` / `DTU_HEAD_SHA`
> (`.gitea/workflows/ci.yml` 自己定义、自己消费,用于提交信息范围检查),属 CI 私有传递,不需要也不应在本机设置。
> 变量值只存路径本身(不加引号);可执行文件变量存 `.exe` 完整路径,目录变量(Kits 根)存目录。
> 不设任何变量时全部走自动发现(PATH / 系统目录推导 / `.venv` / `vswhere`);变量只用于覆盖非标准安装位置。
> 字体系统目录(`simsun.ttc`)不设 DTU 变量:由 `%WINDIR%\Fonts\` 推导,`make_font.py` 的 `--font` 参数仍最高优先。
@@ -37,7 +58,7 @@ CI 侧同理:`DTU_PRECOMMIT_PYTHON` → runner PATH → workspace `.venv`。
1. **显式值校验**:变量已设置时,先校验其指向的文件/目录真实存在且符合形态(如 `cl.exe` 必须位于 `bin\Hostx64\x64\` 下);无效则**立即报错**,错误信息必须包含变量名与所给值,**不得静默回退**到自动发现。
2. **未设置才自动发现**:PATH / 系统目录推导 / `.venv` / `vswhere` 等;自动发现失败按各工具既有降级或报错语义处理。
3. **BAT 实现约束**:`.bat` 中读取环境变量用 `%DTU_...%`;判空用 `if defined`;错误分支 `exit /b` 非零。BAT 内解析一律平铺 `goto` 标签风格,不用括号块(`echo )` 会提前闭合块)。
3. **BAT 实现约束**:仓库内已无 `.bat` 入口(原 `MDK-ARM/build.bat` 2026-10-10 删除,构建走 `/keil-build`);若在别处写 BAT,读取环境变量用 `%DTU_...%`、判空用 `if defined`、错误分支 `exit /b` 非零,且一律平铺 `goto` 标签风格不用括号块(`echo )` 会提前闭合块)。
4. **PowerShell 实现约束**:`$env:DTU_...`;`build_sim.ps1` 的 CMake 配置不直接读环境变量(保持生成器无关)。
5. **workflow run 块**:`ci.yml` 的 run 块内用 `${env:DTU_...}`(未设置时为空串)与 PowerShell `$env:DTU_...` 判空,**不得**在 workflow 文件里写死任何本机绝对路径。
6. **Windows 命令行引号规则**:值含空格(如 `C:\Program Files (x86)\...`)时,调用端必须整体加引号(`"%DTU_KEIL_UV4%"` / `"& $env:DTU_..."`);变量值本身**不要**带引号。
@@ -57,11 +78,14 @@ $env:DTU_STLINK_CLI = 'D:\Tools\ST-LINK Utility\ST-LINK_CLI.exe'
### 3.2 用户级持久配置(日常开发机,推荐)
```powershell
# 当前用户、全部新进程可见;无需管理员
# 当前用户、全部新进程可见;无需管理员(示例路径,按本机实际安装位置替换)
[Environment]::SetEnvironmentVariable('DTU_KEIL_UV4', 'D:\Tools\Keil\UV4\UV4.exe', 'User')
[Environment]::SetEnvironmentVariable('DTU_STLINK_CLI', 'D:\Tools\ST-LINK Utility\ST-LINK_CLI.exe', 'User')
# 已存在同名值时直接覆盖;删除用 [Environment]::SetEnvironmentVariable('DTU_KEIL_UV4', $null, 'User')
# 设置后需重开终端/IDE 才对新进程生效
# 设置后需重开终端/IDE 才对新进程生效。
# ⚠ DSH agent 会话的 shell 由常驻服务派生,不继承服务启动之后设置的用户级变量:
# 设完后当前会话仍看不见,须把值注入单次调用(options.env),或重启宿主服务。
# 这是 /keil-build 报 exit 2「UV4.exe not found」但注册表里明明有值的头号原因。
```
### 3.3 Gitea Windows runner(SYSTEM 服务)
@@ -85,107 +109,75 @@ $env:DTU_STLINK_CLI = 'D:\Tools\ST-LINK Utility\ST-LINK_CLI.exe'
---
## 5. 三个解释器各能跑什么(2026-10-27 实测,道G task-33)
## 5. 门禁解释器环境与修复规程
> 起因:有队友在 `.venv` 里 `import identify.tags` 失败,又用 `pip` 查不到 `pre-commit`,据此判定
> 「`.venv` 里的 pre-commit 安装是残缺的」。**复测结论:环境不残缺,两条观测都成立但推错了因**——
> 前者是版本本身没有那个模块(下表第三行),后者是查错了对象(下表第二行)。
> 取证方法固定:同一个探针脚本(`sys.executable` + 逐个 `import` + `importlib.metadata.version`)
> 分别喂给三个解释器;脚本放仓外临时目录,不入库、不改任何环境。
### 5.1 谁跑在哪个 Python 上
| 解释器 | 身份 | 门禁相关模块 | 第三方包 | 结论 |
|---|---|---|---|---|
| `.venv\Scripts\python.exe` | 3.12.4(**基底是 Anaconda**,非干净 CPython) | `identify` 可导入、`pre_commit` 可导入;`identify.tags` **无此模块** | fontTools 4.51.0 / pillow 10.3.0 / pyserial 3.5 / pyyaml 6.0.3 / cfgv 3.5.0 全在,`pre-commit` 与 `identify` 的 dist-info 齐全(`pip show` 可查) | **就是钩子的执行环境**:`.git/hooks/pre-commit` 里 `INSTALL_PYTHON` 写死指向它 |
| `C:\Python314\python.exe` | 3.14.7,PATH 首位 | 三个全缺 | **一个都没装**(`pip` 元数据全 `PackageNotFoundError`) | 「查不到 pre-commit」的现场就是它;`language: system` 的 42 条 entry 的裸 `python` 落在这里 |
| `C:\Users\...\Python311\python.exe` | 3.11.9,PATH 次位 | 同上全缺 | 同上全缺 | 与门禁无关,列出来只为排除「装了但找不到」的猜测 |
pre-commit 不改 PATH,所以「钩子用哪个解释器」由 entry 怎么写决定。本仓的写法是
**entry 只写 `scripts/gate_python.py` + 脚本名,解释器交给启动器在运行时找**:
**为什么"缺 `identify.tags`"不是故障**:`identify-2.6.19.dist-info/RECORD` 一共 24 行,列出的模块只有
`__init__.py` / `cli.py` / `extensions.py` / `identify.py` / `interpreters.py` / `vendor/licenses.py`——
**根本没有 `identify/tags.py` 这一条**,而磁盘上的文件与 RECORD 逐项一致。即"元数据完好、文件齐全、
只是这个版本没有那个模块"。标签常量在这个版本里位于 `identify/identify.py`(`TEXT` / `BINARY` /
`EXECUTABLE` / `ALL_TAGS`),pre-commit 4.6.2 自己也是从那里取的(`clientlib.py` 里
`from identify.identify import ALL_TAGS`),**全仓没有任何脚本 import `identify.tags`**(实测 grep 零命中)。
⇒ 需要复算文件类型标签时,正确入口是 `identify.identify`;旧写法 `identify.tags` 在这个版本对上必然失败。
| 谁 | 用哪个 Python | 依据 |
|---|---|---|
| pre-commit 本体 | 仓库 `.venv` | 安装时生成的钩子脚本把路径写死在 `INSTALL_PYTHON` 里;跨工作树共用同一份(`git rev-parse --git-common-dir` 指向主树的 `.git`),换树不必重装 |
| `language: system` 钩子(本仓 44 条) | 仓库 `.venv` | entry 是 `scripts/gate_python.py <脚本>`;启动器按候选链解析,见 §5.2 |
| 脚本内部再调 Python(生成器重跑等) | 启动器解析出的那一个 | 启动器用环境变量 `DTU_GATE_PYTHON` 把选中结果传给子进程,脚本内再调 Python 时自动与钩子一致 |
### 5.1 新增钩子 entry 的硬规则(今天没炸的真正原因)
`language: python` 类钩子跑在 pre-commit 自己的缓存环境里(可用 `DTU_PRECOMMIT_PYTHON` 钉住),
与 `.venv` 无关;本仓当前没有这类钩子。PATH 首位是哪个 Python 已不影响门禁。
`language: system` 的 entry 不继承 `.venv`:pre-commit 不改 `PATH`,所以裸 `python` 命中 PATH 首位
(本机 = 3.14.7,**零第三方包**)。实测本仓 45 条 system 钩子里 **43 条 entry 以裸 `python` 开头**,
今天能全绿的原因是(**2026-10-27 二次修正,改以机械口径为准**):
### 5.2 写新钩子:entry 走启动器,不写解释器
1. 这些条目**没有任何一条在模块级 import 第三方包**(抽测 bom-drift / layer-deps / doc-links 与一套
带 harness 的主机单测,在 3.14.7 下 exit 0);
2. 真正会碰第三方的是**下游脚本里的惰性导入**——典型是字体链 `tools/font_gen/make_font.py` 在函数体内
`from fontTools.ttLib import TTCollection`。它会不会被走到静态判不了,所以
`scripts/ci_local.py` 的钩子解释器预检把这类只列成**候选**(实测列出 `font-charset` / `generated-fresh`
两条经下游引入 fontTools),摊开给人看、不作判决;判决只留给「模块级 import + 裸 python +
该解释器确实没装 + 脚本不自带解释器解析」四事齐备那种必然崩。
```yaml
entry: scripts/gate_python.py scripts/check_新检查.py
```
> ⚠ 本节 2026-10-27 的早先版本写作「42 条里唯一需要第三方包的那条自带解释器解析」。那是我拿一条宽松
> 正则(连注释与字符串里的包名都算命中)审计出的**保守过头的结论**,条目数也因 `nested-comment` 钩子
> 落地从 42 变 43。现在这条口径由钩子解释器预检每次跑门禁前重算,人不再需要记这个数。
启动器按顺序找解释器,第一个可用的胜出:
`$DTU_GATE_PYTHON` → `<仓库>/.venv/Scripts/python.exe` → `<仓库>/.gate/python/python.exe`(可选便携解释器)
→ PATH 上的 `python`。一个都找不到时退出码 2,并把四个候选的检查结果打出来。
排查「钩子到底跑在谁身上」:设 `DTU_GATE_PYTHON`,加 `DTU_GATE_VERBOSE=1` 可让它打印选中结果。
⇒ 写新钩子时二选一,别开第三条路:要么脚本只用标准库,要么在脚本内部按
「显式变量 > 当前解释器 > 仓库 `.venv` > PATH」自行解析并**探针校验**(先例与理由见
`docs/门禁说明/generated-fresh.md` §4)。**不要**把 entry 写死成某台机器的绝对路径。
**为什么不是别的写法**(三条实测边界,Windows + pre-commit 4.6.2):
### 5.2 动环境的失效顺序与回滚(含本笔机械化)
- pre-commit **不展开 `${VAR}`** —— `entry: '"${DTU_REPO_ROOT}/..."'` 直接报
``Executable `'${DTU_REPO_ROOT}'` not found``,所以不能用变量拼路径;
- `language: script` + `#!/usr/bin/env python` 看似最干净(配置里连解释器都不用写),但它把决定权交给
**PATH 里的第一个 `python`**——正是本方案要摆脱的不确定源。本机实测它会落到 `C:\Python314`(没装任何依赖),
只是现有钩子恰好只用标准库才没炸;
- `language: script` 的 shebang 只做**纯 PATH 改写**,`env -S python` 里的 `-S` 会被丢掉、
`env pwsh` 这种「让 env 去找一个能写逻辑的解释器」的路子走不通。
**先把"谁依赖谁"钉住,才知道动一下会红在哪一步**(下表每行都是本机实测或代码事实,不是推测):
另一个选项是把便携解释器放进仓库(`.gate/python/`),换来「换机零准备」,代价是仓库多几百 MB。
当前**没有**提供它,所以新机器仍需先装 Python。
| 被动什么 | 第一现场 | 症状 | 还能动什么 |
|---|---|---|---|
| 删 / 重建 `.venv` | `.git/hooks/pre-commit` 里的 `INSTALL_PYTHON` 指它 | `git commit` **当场起不来**:钩子脚本先判 `if [ -x "$INSTALL_PYTHON" ]`,不成立才回落到 PATH 上的 `pre-commit`——本机 `where.exe pre-commit` 报找不到,于是回落分支也是死路 | 重建 `.venv` 后必须 `pip install -r requirements.txt`(含 pre-commit),否则回落分支永远还是死路 |
| 只换 `.venv` 里的包(不删目录) | pre-commit 本体与 `language: python` 钩子各自独立的缓存环境(`~/.cache/pre-commit/repo*/py_env*/`)互不相干 | 只影响 pre-commit 本体;改写型钩子跑在缓存环境里,**看不见** `.venv` 的变动(实测那份缓存环境里连 `identify` 都没装) | 缓存环境坏了要 `pre-commit clean`——它会连别人正在跑的钩子环境一起重建,必须先要到静默窗口 |
| 换 PATH 首位解释器 | 那 43 条裸 python 条目 | 模块级第三方 import 一条都没有 ⇒ 普通情况下无感;但 3.12 → 3.14 这种版本跨度会改变标准库行为,是"靠巧合不炸"的敞口 | 设 `DTU_PRECOMMIT_PYTHON` 只钉得住 pre-commit 本体,**钉不住**裸 python 条目的落点——那只能靠脚本自解析 |
| 改 `requirements.txt` 的版本约束 | 别人机器上的一次 `pip install -r` | 本机无感,别人下次重建环境时才炸 | 这不属于"修本机环境",要改先报属主,别夹在诊断笔里 |
启动器本身必须**纯标准库、单文件、不 import 仓库内模块**:它运行在「解释器还不知道是谁」的时刻。
**回滚按现场分条,别混用**:
### 5.3 出问题先看预检,别急着动环境
- `.venv` 被删 / 建坏:`python -m venv .venv` → `pip install -r requirements.txt` →
`& '.venv\Scripts\python.exe' -m pre_commit --version` 通 → `ci_local.py --preflight-only` 通 → 再跑完整门禁。
- 单包装坏:按修复前那次 `pip freeze`(放仓外,不入库)反向 `pip install <包>==<原版本>`;
**不要** `-U` 顺手把别人的依赖一起动。
- 缓存环境坏:`pre-commit clean` 后跑一次 `ci_local.py` 让它重建;重建期间别提交(几十秒内会拿到半装好的环境)。
- 只有全坏透才删 `.venv` 重来;删前记下 `pip freeze`,并记住本仓 `.venv` 基底是 Anaconda
(换机复制不出来,见上面 §5.2 依赖表与 §5.3 第 5 条)。
`scripts/ci_local.py` 每次开头都跑一遍**钩子解释器预检**(`--preflight-only` 可单跑):报清
"钩子跑在谁身上 / 裸 python 落到谁 / 那个解释器缺什么 / 哪些只是候选风险",确认会崩的直接
exit 2 并指名到钩子。它不改环境、不装包、不写仓库,只保证断链在第一秒就报出来。
**本笔把上面这些机械化了一半**:`scripts/ci_local.py` 新增 **钩子解释器预检**(`--preflight-only`
可单跑,也是 `ci_local.py` 每次的第一段),报清"钩子跑在谁身上 / 裸 python 落到谁 / 那个解释器缺什么 /
哪些只是候选",四事齐备即 exit 2 并指名到钩子。红-绿在临时假仓库上验过(同一份配置里 bad-hook 判 FAIL、
good-hook 判 OK,整体 `rc = 2`)。**它不改环境、不装包、不写仓库**,也不替代上面的修复死顺序——
它只保证一件事:这条链下次断掉时报错发生在第一秒,并且带着钩子的名字。
预检报红之后,按现象对号入座:
### 5.3 什么情况下才需要动环境,以及动之前要走死顺序
| 现象 | 原因 | 处置 |
|---|---|---|
| 删了或重建过 `.venv` 后 `git commit` 当场起不来 | 安装过的钩子脚本里写死了旧 `.venv` 的路径;找不到时回落到 PATH 上的 `pre-commit`——本机 PATH 上没有它,于是报错退出 | 重建 `.venv` 并 `pip install -r requirements.txt`(含 pre-commit),再重跑一次 `pre-commit install` 刷新钩子脚本里的路径 |
| 预检报「入口解释器不可用」 | `.venv` 被删或改名,或换机后 entry 里的旧绝对路径失效 | 确认 `.venv` 在位;换机则按新检出位置重写 44 条 entry 的首段(见 §5.2) |
| 钩子报 `No module named xxx` | 脚本要的包不在 `.venv` 里 | 加进 `requirements.txt` 再装(不要装到别的解释器);别用 `-U` 顺手升级别人的依赖 |
| 改了 `requirements.txt` 的版本约束 | 本机无感,别人下次重建环境时才炸 | 这不属于"修本机环境":要改先报属主,别夹在诊断笔里 |
默认动作是**不动**(本节存在即结论:没有需要重装的东西)。只有出现下列**具体现象**才执行修复,
且必须先要到静默窗口——`pre-commit` 与 `.venv` 是全队共享运行面,中途换包会把别人的一次钩子跑打断:
### 5.4 真要动环境:默认不动,动则走五步
1. 停表:确认没有人在跑提交/门禁/台架(build 与 bench 两类锁都空闲,见 `task-worktree`);
2. 备份指针:记下 `& '.venv\Scripts\python.exe' -m pip freeze > 仓外文件`(不入库);
3. 只补不重建:优先 `pip install --force-reinstall --no-deps <包>==<requirements.txt 里的版本>`,
**不要**顺手 `-U`(那会连带动别人的依赖);确实要整体重建才 `pip install -r requirements.txt`,
且该文件必须保持纯 ASCII(Windows 下 pip 按代码页解码);
4. 自证(缺一不算修完):`& '.venv\Scripts\python.exe' -m pip show pre-commit` 有版本 →
`-m pre_commit --version` 通 → `scripts/check_generated_fresh.py` 与字体字符集钩子在 `.venv`
与 PATH 两种解释器下都 exit 0 → 最后跑一次 `scripts/ci_local.py`;
5. 回滚:新装/升级出问题时按第 2 步的 freeze 反向 `pip install <包>==<原版本>`;**别删 `.venv` 重来**,
删了就把「基底是 Anaconda」这条不可复制性也一起删掉(换机复现不了,见本文 §3 与 `docs/环境与部署/项目部署环境要求.md`)。
`pre-commit` 与 `.venv` 是全队共享运行面,中途换包会把别人正在跑的钩子打断。只有出现具体故障
才动手,且顺序固定:
### 5.4 仍开着的账(不在本次范围,登记给对应属主)
- `DTU_PRECOMMIT_PYTHON` 本机当前**未设**,靠 §1.1 的优先级自动落到 `.venv`;如果哪天 PATH 首位换了
解释器,静态门禁档就会跟着换——把它显式设成 `.venv` 是一条零成本的加固(改的是本机环境,不动仓库)。
- 42 条裸 `python` entry 与两个解释器的版本差(3.14 vs 3.12):今天没有可观测的行为差,但这是
「靠巧合不炸」。要不要把 entry 统一改成显式解释器解析,属门禁属主的决定(同一族的先例见 §5.1)。
- 本机的 `.venv` 基底是 Anaconda 发行版,`python -m venv` 在别的机器上给出的不是同一个东西;
这条差异目前无人记录在 `docs/环境与部署/项目部署环境要求.md` 里。
## 变更记录
| 日期 | 变更 |
|---|---|
| 2026-10-27 | **新增 §5.2 动环境的失效顺序与回滚(原插成 §5.4,本笔按依赖表→死顺序→遗留账重排)+ §5.1 二次修正(道G task-33 修复分支)**:四种现场(删/换 `.venv`、只换包、换 PATH 首位、改 requirements 约束)逐行钉死"第一现场 / 症状 / 还能动什么",回滚按现场分条不混用;`scripts/ci_local.py` 同批加 **钩子解释器预检**(`--preflight-only` 单跑,也是每次门禁的第一段:报"钩子跑在谁身上 / 裸 python 落到谁 / 缺哪些包 / 哪些是惰性候选",四事齐备 exit 2 指名到钩子,红-绿在临时假仓库验过)。§5.1 那条「42 条里唯一需要第三方包的那条自带解释器解析」被更严的口径推翻:按**模块级 import** 判是 0 条,条目数也因 `nested-comment` 落地变 43;fontTools 属下游**惰性**导入,预检只列候选不判决。 | 2026-10-27 | **新增 §5 解释器实测矩阵(道G task-33,只读诊断,未动任何环境)**:三个解释器(仓库 `.venv` / PATH 首位 / PATH 次位)逐个探针实测,钉住三条事实——① `.venv` **不是残缺**:`pre-commit` 与 `identify` 的 dist-info 齐全(`pip show` 可查),`identify-2.6.19` 的 RECORD 里本就没有 `tags.py`,标签常量在该版本的 `identify.identify` 里,pre-commit 自己也是从那里取(旧写法 `identify.tags` 在这个版本对上必失败,全仓零引用);② 钩子执行环境被 `.git/hooks/pre-commit` 的 `INSTALL_PYTHON` 钉死为 `.venv`,而 `language: system` 的 42 条 entry 用裸 `python` → 落到 PATH 首位那个**零第三方包**的解释器,今天没炸是「42 条全只用标准库 + 唯一需要第三方包的那条自带解释器解析」两件事同时成立,据此写下新增 entry 的硬规则(§5.1);③ 「查不到 pre-commit」的现场是查了 PATH 上的解释器。另附 §5.2「默认不动环境;确需修复时的死顺序与回滚(只补不重建、freeze 备份放仓外、按 freeze 反向回滚、禁止删 `.venv` 重来)」与 §5.3 三条待属主的账。| 2026-10-02 | **文档改造**:删除规划阶段内容(设计结论、路径耦合盘点、代码改造映射、分阶段推广与验收、风险与边界、后续任务拆分),保留并重组为现行机制说明——§1 变量清单(含 Python 优先级特别说明)、§2 解析与失败语义、§3 本机配置方法(会话/用户/runner 三级)、§4 验收判据、变更记录;章节重编号,外部引用同步(`gen_refgraph.py` §3→§1、`ci_local.py` §3.1→§1.1、`Gitea_Runner部署指南.md` §5.3→§3.3) |
| 2026-10-02 | **实施完成**:阶段 A~D 落地。① 10 处调用点全部改为"`DTU_*` 变量优先 + 自动发现回退"(显式值无效 = 硬报错、不静默回退):`build/download/reset.bat`(goto 标签式解析,纯 ASCII)、`ci_local.py`(`DTU_PRECOMMIT_PYTHON` 严格校验)、`ci.yml`(`DTU_PRECOMMIT_PYTHON`/`DTU_CPPCHECK`/`DTU_MSVC_CL`/`DTU_WINDOWS_KITS_ROOT`,run 块纯 ASCII)、`gen_refgraph.py`(`DTU_GRAPHVIZ_DOT`→PATH,保留无渲染降级)、`unit_test_harness.py`(`DTU_MSVC_CL`/`DTU_WINDOWS_KITS_ROOT` + `%ProgramFiles*%` 推导取代静态盘符列表)、`build_sim.ps1`(`DTU_VSWHERE`→`%ProgramFiles(x86)%` 推导,去字面 C 盘回退)、`make_font.py`(`%WINDIR%` 推导);`_build.cmd` 确认零调用后删除。② 本机按 §3.2 配置用户级 `DTU_KEIL_UV4`/`DTU_STLINK_CLI`(其余工具本机均可自动发现,未设变量),验证:`@build` BUILD_OK 0/0、`ci_local.py` 静态门禁 15 钩子 + 主机侧纯逻辑单测 278/278 断言全绿、refgraph 重生成(含修复 `引用关系图.json` 末尾换行缺失导致的 end-of-file-fixer 抖动,生成器补 `f.write("\n")`)、`build_sim.ps1` vswhere 推导命中 VS2026(当时模拟器 CMake 配置期暴露**既有**问题:`App/cfg_file.c` 已移入 `App/cfg/` 但 `simulator/CMakeLists.txt` 未同步——与环境变量改造无关;2026-10-02 已修复:CMakeLists 同步 `App/cfg/` + `test/board/` 引用,模拟器全量构建通过)、`reset.bat list` 识别探针。③ 文档收口:`AGENTS.md`/`MDK-ARM/README.md`/`项目部署环境要求.md`(新增 §2.1.1)/`Gitea_Runner部署指南.md`(新增 §5.1.3 机器级变量)/`CI配置指南.md`/`pre-commit使用说明.md`/字体/测试流程/引用图说明/ST-LINK 说明/实机规划/`scripts/README.md` 共 12 篇同步为变量口径。④ `requirements.txt` 增 `pre-commit==4.6.2`(重建 `.venv` 即可跑静态门禁)。⑤ 全量 `pre-commit --all-files` + `ci_local.py` 通过。待办:新 workflow 推送远程后跑一次 CI 两 job 闭环验证;第二台不同安装路径机器的交叉验证(操作清单已固化入 `项目部署环境要求.md` §2.12) |
| 2026-10-02 | 初版规划:盘点 BAT/Python/PowerShell/Gitea workflow 中机器相关工具路径;定义 `DTU_*` 变量清单、优先级、用户级与 SYSTEM runner 配置边界;规划 4 阶段迁移及离线/实机验收范围。未改工具调用代码 |
1. **停表**:确认没人跑提交/门禁/台架(build 与 bench 锁都空闲,见 `task-worktree`);
2. **备份指针**:`& '.venv\Scripts\python.exe' -m pip freeze > 仓外文件`(不入库,回滚全靠它);
3. **只补不重建**:`pip install --force-reinstall --no-deps <包>==<requirements.txt 里的版本>`;
**不要**顺手 `-U`(会连带动别人的依赖);确要整体重建才 `pip install -r requirements.txt`
(该文件必须纯 ASCII,Windows 下 pip 按代码页解码);
4. **自证**(缺一不算修完):`.venv` 里 `pip show pre-commit` 有版本 → `-m pre_commit --version` 通 →
`scripts/ci_local.py --preflight-only` 通过 → 最后完整跑一次 `ci_local.py`;
5. **回滚**:新装出问题按第 2 步的 freeze 反向 `pip install <包>==<原版本>`;**别删 `.venv` 重来**——
删掉就要重建并重装 `requirements.txt`,且 `pre-commit install` 必须重跑一次(钩子脚本里写死的
路径要跟着刷新,见 §5.3 第一行)。