From 2be49dc9dd0d7e4dee44b420454604c41719433b Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Fri, 30 Oct 2026 10:25:05 +0800 Subject: [PATCH] =?UTF-8?q?docs(skills):=20=E6=98=8E=E7=A1=AE=20@build/@te?= =?UTF-8?q?st/@task=20=E4=B8=BA=E6=96=87=E6=A1=A3=E5=88=AB=E5=90=8D?= =?UTF-8?q?=E5=B9=B6=E6=96=B0=E5=A2=9E=20precheck/postmerge=20=E5=91=BD?= =?UTF-8?q?=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AGENTS.md「构建与验证」新增总口径:@build / @test / @task 都是本仓库的 文档别名、不是 shell 命令(2026-10-30 实测:bash 敲 @task / @build 即 command not found,dsh task 被解析成 profile 名报错);`=` 后的命令行 才是可执行形态,DSH 会话显式调用为 /keil-build、/serial-boot-test、 /task-worktree。 - AGENTS.md / docs/README.md / 并行任务工作树方案.md:@task 三处定义位 由「短命令」统一改为「文档别名」,并写入两种实测失败形态 - keil-build / serial-boot-test SKILL.md:定义位点破别名性质;@test 在 skill 内原先没有定义句,补口径 blockquote - task-worktree SKILL.md 与 reference/:whenToUse、标题、症状表、插件桥 去 @task 前缀改指 list / 本 skill;frontmatter 能力清单同步 - task_worktree.py 新增 precheck(树内只读自查:BOM 漂移 / CR / U+FFFD、 docs 逐档门禁、--msgfile 消息 BOM/CRLF/行长/禁词检查,自动补 ci-artifacts/)与 postmerge(主树体积凭证自动诊断:新鲜跳过、红先短链 再只读复检裁定是否升全链;check_doc_links --all;列文件不代提交) - done 快路径打印与门禁拦截提示均指向 postmerge 实测:precheck 对坏消息文件抓到 CRLF、行长 95>80、禁词×3(rc=1); postmerge 端到端「凭证同码新鲜→跳链→doc --all 绿(2967 章节引用 / 214 文档)→列 dirty 文件」rc=0;check_skill_size.py 与 py_compile 通过; frontmatter 改动后 DSH 已用新值真加载过一轮。无 .c/.h 改动,不涉及体积。 --- .dsh/skills/keil-build/SKILL.md | 2 +- .dsh/skills/serial-boot-test/SKILL.md | 2 + .dsh/skills/task-worktree/SKILL.md | 164 +++--------------- .../reference/dsh-plugin-bridge.md | 4 +- .../task-worktree/reference/pitfalls.md | 2 + .../task-worktree/scripts/task_worktree.py | 139 +++++++++++++++ AGENTS.md | 3 +- docs/README.md | 2 +- docs/环境与部署/并行任务工作树方案.md | 6 +- 9 files changed, 176 insertions(+), 148 deletions(-) diff --git a/.dsh/skills/keil-build/SKILL.md b/.dsh/skills/keil-build/SKILL.md index af150dae..d825c7b0 100644 --- a/.dsh/skills/keil-build/SKILL.md +++ b/.dsh/skills/keil-build/SKILL.md @@ -129,7 +129,7 @@ Agent 手工 `lock acquire build` 后忘了 `release`,锁要挂到 TTL 才被 ## 本仓库 -**`@build` = 本 skill**(2026-10-10 起为仓库文档口径):在仓库根目录跑 +**`@build` = 本 skill**(2026-10-10 起为仓库文档口径;**`@build` 是文档别名、不是 shell 命令**——原样敲进终端是 command not found / syntax error,可执行形态永远是下面这条命令行;DSH 会话显式调用 `/keil-build`):在仓库根目录跑 `& '.venv\Scripts\python.exe' .dsh\skills\keil-build\scripts\keil_build.py`—— **bash / `ops_bash` 下要写成 `'.venv/Scripts/python.exe' .dsh/skills/keil-build/scripts/keil_build.py`** (原因见「调用形态」)。 diff --git a/.dsh/skills/serial-boot-test/SKILL.md b/.dsh/skills/serial-boot-test/SKILL.md index 1a16da61..5ca0b380 100644 --- a/.dsh/skills/serial-boot-test/SKILL.md +++ b/.dsh/skills/serial-boot-test/SKILL.md @@ -16,6 +16,8 @@ whenToUse: 需要向目标板烧录或复位后,用串口日志自动判定运 | `.dsh/skills/serial-boot-test/scripts/run_boot_test.py` | 一键闭环:open+clear → 烧录 → wait-idle → 提取 boot log → 烧录回读校验 → 标记校验。串口助手按 `$DSH_SERIAL_TOOL` > 同目录 > `../serial-assistant/scripts/serial_tool.py` 解析 | | `.dsh/skills/serial-boot-test/scripts/verify_log.py` | 对任意日志文件做标记校验(Required/Forbidden/锚点计数/顺序/耗时),可单独用。`--exact` 做**整行 `^…$` 锚定**(子串口径照不出"行还在但行首被吃掉/行内被插字节"那一类损坏)、`--pair` 做跨复位配对、`--allow-residual/--head-char` 服务崩溃注入腿、`--selftest` 跑内置判别用例(**证明这套判据不是空跑**)。四个行级开关(`--exact` / `--pair` / `--allow-residual` / `--head-char`)同时接进了 `run_boot_test.py`,@test 闭环直接可用 | +> **`@test` 是 AGENTS.md 的文档别名,不是 shell 命令**(同 `@build` / `@task`,见 AGENTS「构建与验证」总口径):可执行形态 = 上表 `run_boot_test.py` 命令行(仓库根跑),DSH 会话显式调用 `/serial-boot-test`;项目配置与判据见本仓 `docs/测试说明/流程与规范/自动化测试流程.md`。 + > **判据取向(重要,会复发)**:`--exact` 默认**只锚文本、不钉长度**——字节长度属于某一份构建,钉死它会在无辜改动上假红(`@@LEN=` 是显式逃生口,不是默认)。子串 `--required` 不能用于"可能丢行首/被插字节"的行。来龙去脉与固定动作见本仓 `docs/测试说明/专项验证/启动日志原子性复验流程.md`。 交互式串口操作(open/send/log/wait-idle/status 等)不属于本 skill—— diff --git a/.dsh/skills/task-worktree/SKILL.md b/.dsh/skills/task-worktree/SKILL.md index 0606a695..345983fc 100644 --- a/.dsh/skills/task-worktree/SKILL.md +++ b/.dsh/skills/task-worktree/SKILL.md @@ -1,11 +1,11 @@ --- name: task-worktree description: >- - 并行开发的空间管理(git worktree 生命周期,短命令 `@task`):开任务树与分支、联接共享 `.venv` 与模拟器 SDK、build/bench 独占锁、并发上限与腾位(A)、放不掉的锁(B)、`done` 是合并点不是查看器与补绿链(C)、主树被别的会话弄脏时的手工合并(C2)、门禁指标异常的净室归因(D)、回收非登记树与悬挂台账(E)、与 DSH worktree 插件共存及联接分级(F)、树内跑门禁与消息禁词(G)。规格 = docs/环境与部署/并行任务工作树方案.md;深规程全文在 reference/。 + 并行开发的空间管理(git worktree 生命周期;`@task` 只是文档别名不是可执行命令,DSH 会话显式调用 `/task-worktree`):开任务树与分支、联接共享 `.venv` 与模拟器 SDK、build/bench 独占锁、并发上限与腾位(A)、放不掉的锁(B)、`done` 是合并点不是查看器与补绿链(C)、主树被别的会话弄脏时的手工合并(C2)、门禁指标异常的净室归因(D)、回收非登记树与悬挂台账(E)、与 DSH worktree 插件共存及联接分级(F)、树内跑门禁与消息禁词(G)、树内自查与合并后补绿命令化(`precheck` / `postmerge`)。规格 = docs/环境与部署/并行任务工作树方案.md;深规程全文在 reference/。 whenToUse: >- - 要为新任务开隔离空间、同时推进多个任务、把任务合回主干或回收工作树;或撞到 `new` 报「已有 8 个活跃任务(上限 8)」、`lock release` 报「任务 X 不存在(缺台账)」、`done` 报「门禁未全绿」或「主工作树不干净」而那份脏改动是别的会话/插件留的、两个任务抢 COM6 或 UV4、并集合并残留;或在任务树里跑门禁写输出报 ENOENT、`--all` 的问题全在 `simulator/SDL2` 联接下;或门禁指标异常增长查不出哪个文件造成;或要回收 `@task list` 看不到的树、清 branch/path 都已不存在的悬挂台账;或要确认插件树里 `.venv` 与 `simulator/SDL2` 哪个必须补;或要改并行上限这个数字并同步全仓口径。 + 要为新任务开隔离空间、同时推进多个任务、把任务合回主干或回收工作树;或撞到 `new` 报「已有 8 个活跃任务(上限 8)」、`lock release` 报「任务 X 不存在(缺台账)」、`done` 报「门禁未全绿」或「主工作树不干净」而那份脏改动是别的会话/插件留的、两个任务抢 COM6 或 UV4、并集合并残留;或在任务树里跑门禁写输出报 ENOENT、`--all` 的问题全在 `simulator/SDL2` 联接下;或门禁指标异常增长查不出哪个文件造成;或要回收 `list` 台账看不到的树、清 branch/path 都已不存在的悬挂台账;或要确认插件树里 `.venv` 与 `simulator/SDL2` 哪个必须补;或要改并行上限这个数字并同步全仓口径。 --- -# task-worktree — 多任务并行开发空间(短命令 `@task`) +# task-worktree — 多任务并行开发空间(`@task` 仅为文档别名;DSH 显式调用 `/task-worktree`;可执行形态见「固定命令」) - **执行体** = `.dsh/skills/task-worktree/scripts/task_worktree.py`(纯标准库;拷整个 skill 目录即可换项目用)。 - **深规程全文在 reference/ 下:做 C2 / D / E / F / G 任何一条之前先把对应那篇读完**——本文只留判得出来「要不要读」的判据。 @@ -21,9 +21,10 @@ whenToUse: >- | `done` 做了什么、红了怎么补绿、能不能反悔、怎么算「真完成」 | **C** | | `done` 报「主工作树不干净」,而那份脏是别的会话/插件留的 | **C2** → [merge-dirty-main-tree.md](reference/merge-dirty-main-tree.md) | | 门禁指标异常增长要逐文件归因(主树全程只读) | **D** → [clean-room-measure.md](reference/clean-room-measure.md) | -| `@task list` 里没有的树要回收 / detached / 有在飞内容 / 悬挂台账 | **E** → [reclaim-unregistered.md](reference/reclaim-unregistered.md) | +| `list` 台账里没有的树要回收 / detached / 有在飞内容 / 悬挂台账 | **E** → [reclaim-unregistered.md](reference/reclaim-unregistered.md) | | 插件树缺联接、锁归属 `pw:`、「有插件为何还自持生命周期」 | **F** → [dsh-plugin-bridge.md](reference/dsh-plugin-bridge.md) | | 树内写门禁输出 ENOENT、`--all` 假红、写提交/合并信息、字节体检 | **G** → [gates-in-tree.md](reference/gates-in-tree.md) | +| 收工前树内自查、或合并后体积凭证/台账红了想一条命令补绿 | `precheck` / `postmerge`(固定命令表 + C 节第 5~6 步) | | 退出码读到 0 其实是 2、`tail` 之后误判「有冲突」 | 读输出 | | 谁不许跑 `done`/`abort`、清场前必做什么、任务期哪些文件不许改 | 铁律 1–9 | | 收工前最后一遍自查(各节该留什么证据)/全部实测坑 | [verification.md](reference/verification.md) / [pitfalls.md](reference/pitfalls.md) | @@ -45,6 +46,8 @@ PY=".venv/Scripts/python.exe"; TW=".dsh/skills/task-worktree/scripts/task_worktr | `plugin list|adopt|unadopt [插件树名]` | 插件桥:看清单 / 补共享联接 / 摘联接(**不删树**)→ F 节 | | `done ` | 预演 → 合入主干 → 并集残留审计 → 重生成产物 → 门禁 → 清场 | | `abort ` | 工作树 + 分支一起删,**永久丢弃** | +| `precheck` | 树内自查(**只读**):BOM 漂移 / CR / U+FFFD + docs 逐档门禁 + `--msgfile --ban` 消息检查;顺手补 `ci-artifacts/`(G 节机械项) | +| `postmerge` | 主树合并后补绿:体积凭证**自动诊断**(新鲜跳过;红→先短链 `--update`,只读复检真漂了才升全链)+ `check_doc_links --all` + 列待提交文件,**不代提交** | - **退出码** `0` 成功 / `1` 操作失败 / `2` 前置或环境错误 / `3` 用法——**只有不接管道才读得到**(→ 读输出)。 - `new`:`--base`(默认 `v2-on-ui`)· `--needs-bench`(无台架 PASS 则 `done` 拒合)· `--lean`(跳 LFS smudge,只省磁盘不省时间)· `--no-bootstrap`(不建联接)· `--force`(**只跳过并发上限检查**,不解决谁让位 → A) @@ -108,7 +111,8 @@ git commit -F # 提交属 Ask first(AGENTS.md ``` 为什么必然要补 → 陷阱「`done` 的门禁跑在 `@build` 之前」。合并解过冲突时,**合并提交期 pre-commit 只查冲突文件**(→ G 节第 3 条)→ 落主干后必须另跑全量。 -6. **重跑 `done` 前主树必须干净**,否则 `失败:主工作树不干净(…)——合并会把未提交改动卷进来` → 第 5 步产物先提交。分支已并时重跑打印 `[i] task/- 已合入 ,跳过合并,直接清场`。主树脏且那份脏不是你的 → **C2**。 + **注释/文档-only 但碰了 .c/.h 的短链**:先比 `Program Size` 与合并前是否逐项相同——相同就只 `ram_baseline.py --update` 重写体积凭证(`scripts/size_attestation.json` 跟着补提交笔走),**别 `--write` 动受管区**(等于给在飞分支造冲突面);数字变了才走上面全链。**已命令化**:`"$PY" "$TW" postmerge`——新鲜直接跳过;红则先短链,再用只读 `check_mem_numbers` 复检裁定是否升全链;末尾跑 `--all` 并列出待提交文件(不代提交)。上面的手工全链仍是权威兜底。 +6. **重跑 `done` 前主树必须干净**,否则 `失败:主工作树不干净(…)——合并会把未提交改动卷进来` → 第 5 步产物先提交。分支已并时重跑打印 `[i] task/- 已合入 ,跳过合并,直接清场`——**该路径在门禁之前就返回,不会替你复绿**:上一次门禁没跑到的尾巴(主树 `ci_local.py` + `check_doc_links.py --all`;体积/文档那半可 `postmerge` 一把跑)必须自己补。主树脏且那份脏不是你的 → **C2**。 7. 清场后再敲一次 `done ` 得 `前置/环境错误:任务 不存在(缺台账 …json)`(exit 2)——**不是失败**:清场最后一步 unlink 了台账。**判完成以 `git log` 为准**。 ### C2. `done` 被「主工作树不干净」挡住,而那份脏是**别的并发会话**留的 @@ -141,149 +145,29 @@ git commit -F # 提交属 Ask first(AGENTS.md - **文档门禁树内按单文件跑;全库 `--all` 留到合并后在主树跑**——树内 `simulator/SDL2` 是同一份仓库的联接,`--all` 把它一起扫成假红(实测 RC=1 而问题全在联接下、要改的文档一条没有)。非要树内跑:① 主树同命令拿基线 ② 过滤联接下的问题行 ③ 剩下的逐行比。**只做一步就是误判。** - **合并提交期 pre-commit 只查冲突文件**(日志首行 `[INFO] Checking merge-conflict files only.`,其余全 `(no files to check) Skipped`)→ 合并落主干后**必须另跑全量** `scripts/ci_local.py`。 - **跨层改动拆不成「按层分笔」**:钩子看到的世界 = HEAD + 本笔 staged(未暂存被 stash;未跟踪文件不被 stash 但会拖红同一棵树上别人的提交)→ 登记文件 / 生成物 / 互锁引用的文档分到他笔,本笔必被整仓门禁拦(`doc-links`/`registration-check`/`generated-fresh`/`mem-numbers`)。判据:**一笔提交必须自洽**。 -- **跑门禁前字节体检**:`lines / bytes / BOM / CR / U+FFFD / 三连续空行 / 行尾空格 / 末行单个 LF` + Markdown 表格每行列数一致性(单元格内竖线写成反斜杠竖线,否则被当分隔符断列)。坏行**未必是本次改动**,先定归属。`doc-links`/`no-mojibake` 只覆盖编码那半。 -- **提交/合并信息一律 `-F` 读文件**(UTF-8 无 BOM、标题 ≤100、正文每行 ≤80,放 `ci-artifacts/`);清退类任务加**禁词自检**:`git log -1 --pretty=%B | grep -c '<关键词>'` 必须是 **0**。未推送前发现消息写错 → `--amend -F` 就地修,别追加「更正」提交(等于把禁词写回历史)。 +- **跑门禁前字节体检**:`lines / bytes / BOM / CR / U+FFFD / 三连续空行 / 行尾空格 / 末行单个 LF` + Markdown 表格每行列数一致性(单元格内竖线写成反斜杠竖线,否则被当分隔符断列)。坏行**未必是本次改动**,先定归属。`doc-links`/`no-mojibake` 只覆盖编码那半。机械项已命令化:`"$TW" precheck`(BOM 漂移 / CR / U+FFFD + docs 逐档门禁,只读);列数一致性与归属判定仍人做。 +- **提交/合并信息一律 `-F` 读文件**(UTF-8 无 BOM、标题 ≤100、正文每行 ≤80,放 `ci-artifacts/`);清退类任务加**禁词自检**:`git log -1 --pretty=%B | grep -c '<关键词>'` 必须是 **0**。未推送前发现消息写错 → `--amend -F` 就地修,别追加「更正」提交(等于把禁词写回历史)。BOM/CRLF/行长/禁词一次查:`"$TW" precheck --msgfile <文件> --ban <词1> --ban <词2>…`。 - 全文(第 1~7 条,含 stash 机制与四道整仓门禁现场)→ [reference/gates-in-tree.md](reference/gates-in-tree.md)。 ## 铁律 -1. ** -`done` - 只能在主工作树执行**——主干分支检出在主树,git 不允许同一分支两处检出。 -2. ** -`done` - 不 push**:它结束于本地合入 + 门禁全绿;推送需用户明确许可。 -3. ** -`build` - 与 -`bench` - 都必须串行**(UV4 不能两个实例同时 -`-b` -;COM6、ST-LINK、设备 IP -`192.168.1.100` - 各一份)。 -`build` - 锁由 **@build 自持自释**( -`keil_build.py` -)→ 调用方**不要**手工 acquire/release; -`bench` - 锁**必须手工** acquire → 烧录/ -`@test` - → release -`--bench-pass` -。非登记持有者怎么放 → B 节;插件树 → F 节。 -4. **单写者文档**: -`docs/任务规划/` -(整个文件夹)、 -`AGENTS.md` - 任务表块、 -`MDK-ARM/*.uvprojx` -、 -`.pre-commit-config.yaml` -、内存快照 —— 任务期**不在任务树里改**,留到 -`done` - 统一处理;用户显式豁免时(在任务树写、由 done 合回)要在交付报告里点名这条例外。 -5. **清场必须先摘联接**:脚本先 -`os.rmdir` - 掉 -`.venv` - / -`simulator/SDL2` - 联接再 -`git worktree remove` -。手工拆树前必跑 **E 第 3 步**预扫拿到 -`REPARSE: none` -——否则 git 可能递归进联接把主树 -`.venv` - 一起删掉。 +1. **`done` 只能在主工作树执行**——主干分支检出在主树,git 不允许同一分支两处检出。 +2. **`done` 不 push**:它结束于本地合入 + 门禁全绿;推送需用户明确许可。 +3. **`build` 与 `bench` 都必须串行**(UV4 不能两个实例同时 `-b`;COM6、ST-LINK、设备 IP `192.168.1.100` 各一份)。`build` 锁由 **@build 自持自释**(`keil_build.py`)→ 调用方**不要**手工 acquire/release;`bench` 锁**必须手工** acquire → 烧录 / `@test` → release `--bench-pass`。非登记持有者怎么放 → B 节;插件树 → F 节。 +4. **单写者文档**:`docs/任务规划/`(整个文件夹)、`AGENTS.md` 任务表块、`MDK-ARM/*.uvprojx`、`.pre-commit-config.yaml`、内存快照 —— 任务期**不在任务树里改**,留到 `done` 统一处理;用户显式豁免时(在任务树写、由 done 合回)要在交付报告里点名这条例外。 +5. **清场必须先摘联接**:脚本先 `os.rmdir` 掉 `.venv` / `simulator/SDL2` 联接再 `git worktree remove`。手工拆树前必跑 **E 第 3 步**预扫拿到 `REPARSE: none`——否则 git 可能递归进联接把主树 `.venv` 一起删掉。 6. **活跃任务硬上限 8 = 脚本常量 `MAX_CONCURRENT`(`task_worktree.py:30`)——唯一事实源**:报错文案与 `new` 的插件树提示都插值它,AGENTS.md / 方案文档 / frontmatter / A 节的数字全是引用同步。改这个数字 → 全形态扫描走 `stale-doc-count-sweep` skill + 隔离小仓验证上限真生效;每棵磁盘代价按实测口径重述、别照抄旧数(细节 → 验证清单末条)。满了 exit 2 → A;悬挂台账 → E 第 6 步。 -`MAX_CONCURRENT` -( -`task_worktree.py:30` -)——唯一事实源**:报错文案与 -`new` - 的插件树提示都插值它,AGENTS.md / 方案文档 / frontmatter / A 节的数字全是引用同步。改这个数字 → 全形态扫描走 -`stale-doc-count-sweep` - skill,并另建隔离小仓验证上限真生效;每棵磁盘代价按实测口径重述(du 约 83 MB / -`--lean` - 约 45 MB,方案文档 §4)别照抄旧数。满了 -`new` - 直接 exit 2 → A 节;悬挂台账 → E 第 6 步。 -7. **工作树放仓库同级 -`_wt/` -,绝不在仓库内部**(否则污染 -`git status` -、 -`--all-files` - 与全库搜索)。唯一例外 = 插件写死的仓内 -`.dsh-worktrees/` -(改不了,已用预置 .gitignore + 三个全库扫描排除兜住 → F 节)。 -8. ** -`done` - 是合并点**:属主没提交完就别跑;合并提交进了主干就只用「主树补提交」收尾,**不用 -`reset --hard` -** 反悔 → C 节。 -9. **非登记 / detached 的树绝不跑 -`done` -/ -`abort` -**:两者都以台账为前置(无台账 exit 2),带残留台账时 -`abort --force` - 会绕过内容保全把别人在飞的改动连树一起删 → E 节。 +7. **工作树放仓库同级 `_wt/`,绝不在仓库内部**(否则污染 `git status`、`--all-files` 与全库搜索)。唯一例外 = 插件写死的仓内 `.dsh-worktrees/`(改不了,已用预置 .gitignore + 三个全库扫描排除兜住 → F 节)。 +8. **`done` 是合并点**:属主没提交完就别跑;合并提交进了主干就只用「主树补提交」收尾,**不用 `reset --hard`** 反悔 → C 节。 +9. **非登记 / detached 的树绝不跑 `done` / `abort`**:两者都以台账为前置(无台账 exit 2),带残留台账时 `abort --force` 会绕过内容保全把别人在飞的改动连树一起删 → E 节。 ## 陷阱与验证(收工前各扫一遍) -- [reference/pitfalls.md](reference/pitfalls.md): -`done` - 的门禁跑在 -`@build` - 之前 · 门禁 = -`pre-commit run --all-files` -- [reference/pitfalls.md](reference/pitfalls.md):全部实测坑逐条(含 `--dry-run` 不保护清场段、门禁跑在 `@build` 之前、未跟踪文件不留痕、并集合并残留、新树从未构建 → 体积类门禁必然红)。收工前扫一眼。 -`git log` - 与合并里都不留痕 · 并集合并残留 · 新树从未构建 → 体积类门禁必然红 · -`--force` - 只覆盖四类检查 · 联接不是 symlink( -`core.symlinks=false` -)· 锁不绑进程存活。 -- [reference/verification.md](reference/verification.md):每节要留的证据(预演 sha + -- [reference/verification.md](reference/verification.md):每节要留的证据(预演 sha + `MT_RC=0`、blob 对账、两把锁空闲、禁词计数 0、`REPARSE: none`、改上限数字后的全形态扫描)。 -、blob id 对账、两把锁空闲、禁词计数 0、 -`REPARSE: none` -…)。 +- [reference/pitfalls.md](reference/pitfalls.md):全部实测坑逐条(含 `--dry-run` 不保护清场段、门禁跑在 `@build` 之前、未跟踪文件在 `git log` 与合并里都不留痕、并集合并残留、新树从未构建 → 体积类门禁必然红、注释-only 也刷红体积凭证、「已合入」快路径在门禁前返回不替你复绿、`--force` 只覆盖四类检查、联接不是 symlink(`core.symlinks=false`)、锁不绑进程存活、文件工具先读后改按绝对路径记账、中文门禁回显乱码但 rc 可信)。收工前扫一眼。 +- [reference/verification.md](reference/verification.md):每节要留的证据(预演 sha + `MT_RC=0`、blob id 对账、两把锁空闲、禁词计数 0、`REPARSE: none`、改上限数字后的全形态扫描)。 ## 与其它 skill 的衔接 -- 构建 = `keil-build`(`@build`)· 烧录/回读 = `stlink-flash` · 串口闭环 = `serial-boot-test`(真实 hex 路径以 `.serialtest/config.json` 的 hex 键为准,拷错 = 烧旧镜像) -`keil-build` -( -`@build` -)· 烧录/回读 = -`stlink-flash` - · 串口闭环 = -`serial-boot-test` -(真实 hex 路径以 -`.serialtest/config.json` - 的 hex 键为准,实测 -`MDK-ARM/Objects/STM32F407-Demo.hex` - 不存在,拷错 = 烧旧镜像) -- 新 -`.c` - 双端注册 = -`new-source-file` - · RAM/.map 台账 = -`mem-budget` - · UI 回归 = -`ui-verify` - · 计数同步 = -`stale-doc-count-sweep` - · 卡死的技能台账行 = -`mnemon-skill-ledger-stuck-repair` -- DSH worktree 插件( -`/worktree` - / -`worktree_create` -,仓内 -`.dsh-worktrees/` -)与本 skill 的分工、桥接命令、锁归属 → F 节 · 完整机制与目录分配 → -`docs/环境与部署/并行任务工作树方案.md` +- 构建 = `keil-build`(`@build`)· 烧录/回读 = `stlink-flash` · 串口闭环 = `serial-boot-test`(真实 hex 路径以 `.serialtest/config.json` 的 hex 键为准——实测 `MDK-ARM/Objects/STM32F407-Demo.hex` 不存在,拷错 = 烧旧镜像) +- 新 `.c` 双端注册 = `new-source-file` · RAM/.map 台账 = `mem-budget` · UI 回归 = `ui-verify` · 计数同步 = `stale-doc-count-sweep` · 卡死的技能台账行 = `mnemon-skill-ledger-stuck-repair` +- DSH worktree 插件(`/worktree` / `worktree_create`,仓内 `.dsh-worktrees/`)与本 skill 的分工、桥接命令、锁归属 → F 节 · 完整机制与目录分配 → `docs/环境与部署/并行任务工作树方案.md` diff --git a/.dsh/skills/task-worktree/reference/dsh-plugin-bridge.md b/.dsh/skills/task-worktree/reference/dsh-plugin-bridge.md index 6b02b081..e7b6575b 100644 --- a/.dsh/skills/task-worktree/reference/dsh-plugin-bridge.md +++ b/.dsh/skills/task-worktree/reference/dsh-plugin-bridge.md @@ -2,7 +2,7 @@ 本仓现在有**两套**隔离空间。别混用,也别互相回收: -| | `@task`(本 skill) | DSH worktree 插件(`dsh-task-worktree`) | +| | 本 skill(文档别名 `@task`,DSH 显式调用 `/task-worktree`) | DSH worktree 插件(`dsh-task-worktree`) | |---|---|---| | 树在哪 | 仓库**外** `../_wt/-/` | 仓库**内** `.dsh-worktrees/worktree//` | | 谁建 | `new `(人来敲命令) | 会话选「Worktree模式」/ 模型调 `worktree_create`(零命令) | @@ -47,7 +47,7 @@ `task_worktree.py: plugin_lock_owner()`)→ `lock status` 不再一律落成 `build-cli`。 - 上台架仍须手工占锁、用同一个名:`lock acquire bench --task pw: --note <说明>` → `lock release bench --task pw: --bench-pass`。插件树没有台账 → 按 B 节只 warn、锁照常放。 -- **命名建议**:插件 `` 沿用 `@task` 的 `-` 形态(如 `N21-dhara-isfree`),两套视图同名好对账; +- **命名建议**:插件 `` 沿用本 skill 的 `-` 形态(如 `N21-dhara-isfree`),两套视图同名好对账; 但分支前缀必然不同(`worktree/` vs `task/`),别把两者说成同一个分支。 - **收尾不在这**:`done` / `abort` 拿插件树名下手只会 exit 2(这是保护,别绕过去手工 `git worktree remove`; 要拆先 `plugin unadopt` 摘联接,再由**用户** `/worktree remove`)。`/worktree bring-back` 要求主工作树干净, diff --git a/.dsh/skills/task-worktree/reference/pitfalls.md b/.dsh/skills/task-worktree/reference/pitfalls.md index 2588b076..838a1947 100644 --- a/.dsh/skills/task-worktree/reference/pitfalls.md +++ b/.dsh/skills/task-worktree/reference/pitfalls.md @@ -130,3 +130,5 @@ `done` 的门禁自己持锁并给子进程设 `DTU_LOCK_HELD=build` 告知跳过(新旧脚本都兼容)。 **`bench` 锁仍须 Agent 手工持**(会话作用域:跨烧录→`@test`→探针,见 `keil-build` SKILL.md)。 - **⚠ `done --dry-run` 只保护预演段、不保护清场段**(2026-10-29 实测):`done --dry-run --skip-build --skip-gates` 在「分支已合入主干」这条路上**照样执行清场**——摘 `.venv` / `simulator/SDL2` 联接、删分支、清台账,然后打「完成(分支此前已合入)」。想真·零改动就只跑不带 `--skip-gates` 的 `--dry-run` 并停在预演输出;用完一律 `git worktree list` 复验(判完成以 `git log` 为准,不是以有没有再敲一次 `done` 为准)。另:`--dry-run` 的前置检查同样要抢 `build` 锁——被别的任务占着时它会直接报「独占资源 build 仍被 X 占用,先 release」,此时加 `--skip-build` 或先 release,**别为了预演去抢别人的锁**。 +- **⚠ 文件工具「先读后改」按解析后的绝对路径记账,切树后要重新读**(2026-10-30 实测,屏板链路任务树):开新树后先用**相对路径**读过一批文件(读历史记到当时解析到的主树文件上),再进任务树直接 `edit` → 6 条并行编辑全被 `file has not been read — read the file, then retry` 拒。判据:**树内改动一律用树内绝对路径,先 read 再改**——读历史不跨路径复用,工具不做「同名相对路径」宽容。 +- **⚠ 中文门禁输出回显乱码 ≠ 路径坏**(2026-10-30 实测):对含中文文件名的文档跑 `check_doc_links.py`,终端回显整段乱码,但 **rc 与判定计数全部正确**。只认 `[SUMMARY]` 行与退出码,别误诊成「文档丢失」;脚本化批量调用走 python subprocess + 显式 `encoding`,别经命令行中转中文名。 diff --git a/.dsh/skills/task-worktree/scripts/task_worktree.py b/.dsh/skills/task-worktree/scripts/task_worktree.py index 5bf72d27..84703a01 100644 --- a/.dsh/skills/task-worktree/scripts/task_worktree.py +++ b/.dsh/skills/task-worktree/scripts/task_worktree.py @@ -970,6 +970,8 @@ def cmd_done(a) -> int: raise Fail("任务分支相对 {} 没有新提交,无需合并".format(base)) if already and ahead == 0: print("[i] {} 已合入 {},跳过合并,直接清场".format(branch, base)) + print("[i] 该路径在门禁之前就返回——不替你复绿;上一次门禁没跑到的尾巴," + "清场后去主树跑 `postmerge`(或手工 ci_local + check_doc_links --all)") print("=== 清场 ===") teardown_worktree(st, force=a.force, base=base) print("\n完成(分支此前已合入)。") @@ -1020,6 +1022,7 @@ def cmd_done(a) -> int: print("!! 门禁未全绿:工作树与分支**保留**,未清场。") print(" 主干已含合并提交 {}".format(base_after[:12])) print(" 可选:a) 在主工作树修正后追加提交(推荐)") + print(" ——体积凭证/台账红就用 `postmerge` 自动诊断回填,别手抄补链") print(" b) 回退本次合并:git -C \"{}\" reset --hard {}".format(main, base_before[:12])) return EXIT_FAIL @@ -1031,6 +1034,130 @@ def cmd_done(a) -> int: return EXIT_OK +# ------------------------------------------------- 树内预检 / 合并后补绿(机械化) + + +HYGIENE_BINARY = (".png", ".jpg", ".gif", ".ico", ".ttf", ".bin", ".hex", ".elf", + ".axf", ".o", ".crf", ".lst", ".zip", ".gz", ".pdf", ".cur") +CR_EXEMPT = (".bat", ".cmd") # .gitattributes 强制 CRLF + + +def _git_bytes(*args, cwd=None): + r = subprocess.run(["git"] + [str(x) for x in args], cwd=str(cwd or REPO_ROOT), + capture_output=True, stdin=subprocess.DEVNULL) + return r.stdout if r.returncode == 0 else None + + +def _changed_paths(cwd: Path): + q = ("-c", "core.quotePath=false") # 中文文件名不许被八进制转义出去 + tracked = git(*q, "diff", "--name-only", "HEAD", cwd=cwd, check=False).stdout + others = git(*q, "ls-files", "--others", "--exclude-standard", cwd=cwd, check=False).stdout + return ([p.strip() for p in tracked.splitlines() if p.strip()], + [p.strip() for p in others.splitlines() if p.strip()]) + + +def cmd_precheck(a) -> int: + """树内自查(只读):字节体检 + docs 逐档门禁 + 可选提交信息检查。绝不代改。""" + cwd = Path.cwd() + (cwd / "ci-artifacts").mkdir(exist_ok=True) # 新树没有 ci-artifacts(G 节:ENOENT 坑) + tracked, untracked = _changed_paths(cwd) + print("[i] {}:改动 {} 个(tracked {} + untracked {})".format( + cwd, len(tracked) + len(untracked), len(tracked), len(untracked))) + bom3, cr, fffd = bytes.fromhex("efbbbf"), b"\r", bytes.fromhex("efbfbd") + problems, docs = 0, [] + for p in tracked + untracked: + f = cwd / p + if not f.is_file() or p.lower().endswith(HYGIENE_BINARY): + continue + b = f.read_bytes() + bad = [] + if cr in b and not p.lower().endswith(CR_EXEMPT): + bad.append("CR") + n = b.count(fffd) + if n: + bad.append("U+FFFD x{}".format(n)) + if p in tracked: + hb = _git_bytes("show", "HEAD:" + p, cwd=cwd) + if hb is not None and hb.startswith(bom3) and not b.startswith(bom3): + bad.append("BOM 漂移(HEAD 有 BOM、工作区没了——bom-guard 会拦)") + if p.startswith("docs/") and p.endswith(".md"): + docs.append(p) + if bad: + problems += 1 + print("!! {}:{}".format(p, " / ".join(bad))) + for d in docs: + if not run_script("scripts/check_doc_links.py", [d], cwd, "文档门禁 " + d, tail=6): + problems += 1 + if a.msgfile: + m = Path(a.msgfile) + if not m.is_file(): + raise EnvError("消息文件不存在:{}".format(m)) + mb = m.read_bytes() + if mb.startswith(bom3): + problems += 1 + print("!! 消息带 BOM(commit -F 要 UTF-8 无 BOM)") + if cr in mb: + problems += 1 + print("!! 消息含 CRLF") + ml = mb.decode("utf-8", "replace").splitlines() + for i, ln in enumerate(ml): + lim = 100 if i == 0 else 80 + if len(ln) > lim: + problems += 1 + print("!! 消息第 {} 行长 {} > {}".format(i + 1, len(ln), lim)) + break + for w in a.ban: + c = mb.decode("utf-8", "replace").lower().count(w.lower()) + print("[i] 禁词 {!r}:{} 次".format(w, c)) + if c: + problems += 1 + print("完成:问题 {} 个".format(problems)) + return EXIT_OK if problems == 0 else EXIT_FAIL + + +def cmd_postmerge(a) -> int: + """主树合并后补绿:体积凭证自动诊断(短链/全链)+ 文档全量。列文件,不提交。 + + 裁定外包给现有门禁:凭证红 → 先短链(--update 重写)→ 用只读的 + check_mem_numbers 复检裁定数字是否真变——红了才升级全链(--write 族)。 + """ + main = main_worktree() + if Path.cwd().resolve() != main.resolve(): + raise EnvError("postmerge 必须在主工作树跑:{}".format(main)) + if not list((main / "MDK-ARM").glob("**/*.map")): + raise EnvError("主树没有 .map——先 @build 0/0 再回来(C.5 的前提)") + gate = "scripts/check_size_gate.py" + if not a.force and run_script(gate, ["--local"], main, "体积凭证检查(同码)", tail=8): + print("[ok] 凭证同码新鲜,无需补链") + else: + run_script("scripts/ram_baseline.py", ["--update"], main, "短链:重写体积凭证", tail=6) + mem_ok = (a.skip_mem or run_script("scripts/check_mem_numbers.py", [], main, + "受管区一致性复检(只读)", tail=8)) + if not mem_ok: + print("[i] 快照与 .map 真漂了 → 升级全链(数字变了才动受管区)") + run_script("scripts/check_mem_numbers.py", ["--write"], main, "同步内存快照") + run_script("scripts/ram_baseline.py", ["--write"], main, "同步 RAM 预算台账") + if a.coverage: + run_script("scripts/check_test_coverage.py", ["--update-baseline"], main, + "覆盖率基线刷新", tail=6) + if not run_script(gate, ["--local"], main, "凭证复检(期望绿)", tail=8): + print("!! 凭证复检仍红——贴输出查 check_size_gate 原因,别硬提交") + return EXIT_FAIL + ok = True + if not a.skip_docs: + ok = run_script("scripts/check_doc_links.py", ["--all"], main, "文档全库检查", tail=8) + dirty = git("-c", "core.quotePath=false", "diff", "--name-only", "HEAD", + cwd=main, check=False).stdout.splitlines() + dirty = [x.strip() for x in dirty if x.strip()] + if dirty: + print() + print("[i] 回填产物(提交属 Ask first,本命令不代提交):") + for p in dirty: + print(" - " + p) + print(" 建议:git add <上面列的文件> && git commit -F <正文文件>") + return EXIT_OK if ok else EXIT_FAIL + + def cmd_abort(a) -> int: st = load_state(a.task_id) wt = Path(st["path"]) @@ -1116,6 +1243,18 @@ def build_parser(): p.add_argument("task_id") p.add_argument("--force", action="store_true") p.set_defaults(func=cmd_abort) + + p = sub.add_parser("precheck", help="树内自查(只读):字节体检 + docs 逐档门禁 + 可选消息检查") + p.add_argument("--msgfile", default=None, help="附带检查提交信息文件(BOM/CRLF/行长/禁词)") + p.add_argument("--ban", action="append", default=[], help="禁词,可重复") + p.set_defaults(func=cmd_precheck) + + p = sub.add_parser("postmerge", help="主树合并后补绿:体积凭证自动诊断(短/全链)+ 文档全量;不代提交") + p.add_argument("--force", action="store_true", help="凭证新鲜也强制走一遍 --update") + p.add_argument("--coverage", action="store_true", help="附带覆盖率基线刷新") + p.add_argument("--skip-docs", action="store_true", help="跳过 check_doc_links --all") + p.add_argument("--skip-mem", action="store_true", help="只挪凭证,跳过受管区复检/全链") + p.set_defaults(func=cmd_postmerge) return ap diff --git a/AGENTS.md b/AGENTS.md index 5078abc4..6a08a706 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -79,6 +79,7 @@ ## 构建与验证 - **门禁时机(用户裁定 2026-10-13)**:`@build` / `scripts/ci_local.py` / `check_doc_links.py` **只在准备提交时跑**(用户让 commit 后统一跑一遍),**不是每次中间改动后都跑**——多步改动期间直接改即可,验证集中在提交门禁;`@test` 硬件闭环仍按任务需要跑 +- **`@build` / `@test` / `@task` 都是本仓库的文档别名,不是 shell 命令**(2026-10-30 实测:bash 里直接敲 `@task` / `@build` 是 command not found,`dsh task` 会被解析成 profile 名报错)——下面 `=` 后的命令行才是可执行形态(PowerShell 形态,bash 下路径正斜杠);DSH 会话显式调用 skill 用 `/keil-build`、`/serial-boot-test`、`/task-worktree` - `@build` = `& '.venv\Scripts\python.exe' .dsh\skills\keil-build\scripts\keil_build.py`(仓库根执行)。退出码 **0=PASS / 1=构建失败 / 2=环境错误(UV4/工程/档案)/ 3=用法 / 4=build 锁被占用**;日志 `MDK-ARM/build_log.txt`;换机必设 `DTU_KEIL_UV4` → 细节 **`keil-build` skill** + `docs/环境与部署/本地工具路径环境变量方案.md` - `@test` = `& '.venv\Scripts\python.exe' .dsh\skills\serial-boot-test\scripts\run_boot_test.py`:开串口+清日志 → 烧录(**固件身份戳**保证烧进去的就是这份构建、**回读校验**)→ wait-idle → 采日志 → 标记校验(含启动锚点计数)。退出码 **0=PASS / 1=FAIL / 2=采集或烧录错误 / 3=用法**;戳提取规则、锚点计数判据与按模块定制标记见 `docs/测试说明/流程与规范/自动化测试流程.md` - Forbidden 标记含 `[ERR]`:**四个 Fault 的崩溃日志都以 `[ERR]` 开头 → 出现 Fault 即本次不通过**(刻意) @@ -185,6 +186,6 @@ | 串口助手 | **`serial-assistant` skill**(命令/判据/陷阱,用到自动加载)、`docs/测试说明/测试工具/串口助手使用指南.md`、`串口助手设计说明.md` | | UI 架构 / 模拟器 / UI 自动化回归 | **`lvgl-simulator` skill**(构建/自检/报错速查,用到自动加载)、`docs/UI设计/UI_设计架构.md`、`LVGL模拟器部署与使用说明.md`、`UI_自动化测试流程.md`;UI 改动回归走 **`ui-verify` skill** | | 待办与审计发现登记簿 | `docs/任务规划/README.md` | -| 多任务并行开发(开空间 / 合并回收 / 台架与构建排队);**DSH worktree 插件的仓内树怎么接上**(`.dsh-worktrees/` 缺 `.venv`、`plugin adopt`、锁归属 `pw:`) | **`task-worktree` skill**(`@task`,用到自动加载;插件桥 → 其 F 节)、`docs/环境与部署/并行任务工作树方案.md` §3.4 | +| 多任务并行开发(开空间 / 合并回收 / 台架与构建排队);**DSH worktree 插件的仓内树怎么接上**(`.dsh-worktrees/` 缺 `.venv`、`plugin adopt`、锁归属 `pw:`) | **`task-worktree` skill**(`@task` 仅为文档别名非可执行命令,显式调用 `/task-worktree`,用到自动加载;插件桥 → 其 F 节)、`docs/环境与部署/并行任务工作树方案.md` §3.4 | | 换机 / 工具路径环境变量 | `docs/环境与部署/` | | 远程 CI(Gitea Actions,**只跑本机一台 runner**) | [`docs/CI/README.md`](docs/CI/README.md) | diff --git a/docs/README.md b/docs/README.md index 29fbf3dc..59e5d07a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -252,7 +252,7 @@ docs/ | 文档 | 内容 | |---|---| | [`项目部署环境要求.md`](环境与部署/项目部署环境要求.md) | 开发机软件环境、目标板资源预算、现场部署与验收步骤 | -| [`并行任务工作树方案.md`](环境与部署/并行任务工作树方案.md) | **多任务并行开发机制**:`git worktree` 隔离、目录与共享/独占分配(junction 规则)、`task-worktree`(`@task`)SKILL 命令面、单写者文档与合并残留审计、台架锁;**已落地并端到端实测**(见其 §11) | +| [`并行任务工作树方案.md`](环境与部署/并行任务工作树方案.md) | **多任务并行开发机制**:`git worktree` 隔离、目录与共享/独占分配(junction 规则)、`task-worktree` SKILL 命令面(`@task` 仅为文档别名,显式调用 `/task-worktree`)、单写者文档与合并残留审计、台架锁;**已落地并端到端实测**(见其 §11) | | [`CI/README.md`](CI/README.md) | **远程 CI(Gitea Actions)唯一入口**:服务端开关 / runner 部署与保活 / workflow / 排障。**只涉及本机一台 runner `win-stm32-ci`,其他机器与项目都不涉及**;日常改代码用本地门禁 `scripts/ci_local.py`,不必读这里 | ### 9.3 台账与外部资料 diff --git a/docs/环境与部署/并行任务工作树方案.md b/docs/环境与部署/并行任务工作树方案.md index ba81f508..dbb9b5e0 100644 --- a/docs/环境与部署/并行任务工作树方案.md +++ b/docs/环境与部署/并行任务工作树方案.md @@ -100,7 +100,7 @@ D:/Code/DTU Code/ 本仓现在有**两套**隔离空间并存,这是**有意接受**的现状,不是待清理的重复建设: -| 维度 | 本方案(`@task` skill) | DSH 插件 `dsh-task-worktree` | +| 维度 | 本方案(`task-worktree` skill) | DSH 插件 `dsh-task-worktree` | |---|---|---| | 树的位置 | 仓库外 `../_wt/`(§3.1) | 仓库内 `.dsh-worktrees/worktree/`(插件写死,不可配置) | | 面向的粒度 | 任务(周级、要合并回主干、要过门禁) | 会话(一次对话一个空间,零命令起步) | @@ -110,7 +110,7 @@ D:/Code/DTU Code/ 两条**并发红线由本方案单方守住**(插件不感知):`build`(UV4 单实例)与 `bench`(COM6 / ST-LINK / 设备 IP 唯一)这两把锁的位置与 TTL 口径见 §7.1 / §7.2——插件树里跑 `@build` 时,`keil_build.py` 会照旧自持自释,持有者名从 cwd 反推为 `pw:`;`bench` 仍须手工占同一个名。共享联接(§3.3) -插件不会建,由 `@task plugin adopt ` 补。 +插件不会建,由本 skill 的 `plugin adopt ` 补(执行体命令行见 §8)。 **为什么允许它在仓内破例**(与 §10「绝不在仓库内部」的口径冲突处):路径根写死在插件里,且暴露给 模型的工具只有 create/list/status 三个。兜法是把该目录登记进**全库扫描**的排除面(`check_doc_links` @@ -264,7 +264,7 @@ D:/Code/DTU Code/ ## 8. SKILL 设计 -**名称**:`task-worktree` **短命令**:`@task`(沿用本仓库 `@build` / `@test` 的 `@` 惯例) +**名称**:`task-worktree` **文档别名**:`@task`——**只是沿用 `@build` / `@test` 惯例的文档记号,没有任何执行机制**:shell 里敲 `@task` 直接 command not found,`dsh task` 会被当 profile 名报错(2026-10-30 实测);DSH 会话里的显式调用是 `/task-worktree`,可执行形态只有下面的执行体命令行。 **执行体**:`.dsh/skills/task-worktree/scripts/task_worktree.py`(纯标准库, 照 `keil_build.py` 的写法,整目录可拷到别的项目复用)。**已落地并端到端实测**(见 §11)。