docs(skills): 明确 @build/@test/@task 为文档别名并新增 precheck/postmerge 命令
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 改动,不涉及体积。
This commit is contained in:
@@ -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`**
|
||||
(原因见「调用形态」)。
|
||||
|
||||
@@ -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=<n>` 是显式逃生口,不是默认)。子串 `--required` 不能用于"可能丢行首/被插字节"的行。来龙去脉与固定动作见本仓 `docs/测试说明/专项验证/启动日志原子性复验流程.md`。
|
||||
|
||||
交互式串口操作(open/send/log/wait-idle/status 等)不属于本 skill——
|
||||
|
||||
@@ -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:<name>`、「有插件为何还自持生命周期」 | **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 <ID>` | 预演 → 合入主干 → 并集残留审计 → 重生成产物 → 门禁 → 清场 |
|
||||
| `abort <ID>` | 工作树 + 分支一起删,**永久丢弃** |
|
||||
| `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 <UTF-8 无 BOM 的正文文件> # 提交属 Ask first(AGENTS.md
|
||||
```
|
||||
|
||||
为什么必然要补 → 陷阱「`done` 的门禁跑在 `@build` 之前」。合并解过冲突时,**合并提交期 pre-commit 只查冲突文件**(→ G 节第 3 条)→ 落主干后必须另跑全量。
|
||||
6. **重跑 `done` 前主树必须干净**,否则 `失败:主工作树不干净(…)——合并会把未提交改动卷进来` → 第 5 步产物先提交。分支已并时重跑打印 `[i] task/<ID>-<slug> 已合入 <base>,跳过合并,直接清场`。主树脏且那份脏不是你的 → **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/<ID>-<slug> 已合入 <base>,跳过合并,直接清场`——**该路径在门禁之前就返回,不会替你复绿**:上一次门禁没跑到的尾巴(主树 `ci_local.py` + `check_doc_links.py --all`;体积/文档那半可 `postmerge` 一把跑)必须自己补。主树脏且那份脏不是你的 → **C2**。
|
||||
7. 清场后再敲一次 `done <ID>` 得 `前置/环境错误:任务 <ID> 不存在(缺台账 …json)`(exit 2)——**不是失败**:清场最后一步 unlink 了台账。**判完成以 `git log` 为准**。
|
||||
|
||||
### C2. `done` 被「主工作树不干净」挡住,而那份脏是**别的并发会话**留的
|
||||
@@ -141,149 +145,29 @@ git commit -F <UTF-8 无 BOM 的正文文件> # 提交属 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`
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
本仓现在有**两套**隔离空间。别混用,也别互相回收:
|
||||
|
||||
| | `@task`(本 skill) | DSH worktree 插件(`dsh-task-worktree`) |
|
||||
| | 本 skill(文档别名 `@task`,DSH 显式调用 `/task-worktree`) | DSH worktree 插件(`dsh-task-worktree`) |
|
||||
|---|---|---|
|
||||
| 树在哪 | 仓库**外** `../_wt/<ID>-<slug>/` | 仓库**内** `.dsh-worktrees/worktree/<name>/` |
|
||||
| 谁建 | `new <ID> <slug>`(人来敲命令) | 会话选「Worktree模式」/ 模型调 `worktree_create`(零命令) |
|
||||
@@ -47,7 +47,7 @@
|
||||
`task_worktree.py: plugin_lock_owner()`)→ `lock status` 不再一律落成 `build-cli`。
|
||||
- 上台架仍须手工占锁、用同一个名:`lock acquire bench --task pw:<name> --note <说明>` →
|
||||
`lock release bench --task pw:<name> --bench-pass`。插件树没有台账 → 按 B 节只 warn、锁照常放。
|
||||
- **命名建议**:插件 `<name>` 沿用 `@task` 的 `<ID>-<slug>` 形态(如 `N21-dhara-isfree`),两套视图同名好对账;
|
||||
- **命名建议**:插件 `<name>` 沿用本 skill 的 `<ID>-<slug>` 形态(如 `N21-dhara-isfree`),两套视图同名好对账;
|
||||
但分支前缀必然不同(`worktree/` vs `task/`),别把两者说成同一个分支。
|
||||
- **收尾不在这**:`done` / `abort` 拿插件树名下手只会 exit 2(这是保护,别绕过去手工 `git worktree remove`;
|
||||
要拆先 `plugin unadopt` 摘联接,再由**用户** `/worktree remove`)。`/worktree bring-back` 要求主工作树干净,
|
||||
|
||||
@@ -130,3 +130,5 @@
|
||||
`done` 的门禁自己持锁并给子进程设 `DTU_LOCK_HELD=build` 告知跳过(新旧脚本都兼容)。
|
||||
**`bench` 锁仍须 Agent 手工持**(会话作用域:跨烧录→`@test`→探针,见 `keil-build` SKILL.md)。
|
||||
- **⚠ `done --dry-run` 只保护预演段、不保护清场段**(2026-10-29 实测):`done <ID> --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`,别经命令行中转中文名。
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user