ecf9d3ad3d
围栏腐坏是真 bug:@T3@ 是被误提交进仓的代码围栏占位符,Markdown 渲染出 来就是一行 @T3@ 加裸命令。git log -S 定位:842d711e(2026-10-28) SKILL.md 还有 18 个真三反引号围栏行、0 个占位符;21c68521(2026-10-29 插件桥接那笔)起变 0 围栏 / 34 个占位符。全仓只有本技能 6 个文件有此 病,draw-diagram / keil-build / ui-verify 等都用真围栏。本轮七个文件 占位符清零、围栏全部成对。 信息密度:SKILL.md 28,139 → 23,087 B(−18%,占插件写侧上限 65536 的 35%,给自进化留头量)。同一句外置理由在 C2/D/E/F/G/陷阱/验证 重复 7 次 → 0;frontmatter 与 F 节写了两遍的插件桥接说明收进 F 节;「这不是免费 的」「挡给你看的保护」这类修辞删掉。删掉的细节全部落进对应 reference 篇——skill-size 钩子会校验指针不悬空:现 26 处链接 / 8 篇目标 / 悬空 0, 每篇都有入口。 锚点未重排:节号 A–G/C2、各节步骤号、铁律编号被 reference/ 反向引用 (铁律 3/5/7/9 与 A 第 1、5 步,C 第 5~7 步,E 第 1~6 步均结构性存在)。 只在 SKILL.md 出现的 PIPESTATUS 教训保留(reference/ 里没有,删了就丢)。 门禁:check_skill_size.py rc=0(17 个 SKILL.md 全在写侧上限内、无悬空 指针);七文件均无 BOM / CR / U+FFFD、单个尾换行。
87 lines
7.4 KiB
Markdown
87 lines
7.4 KiB
Markdown
### C2. `done` 被「主工作树不干净」挡住,而那份脏改动是**别的并发会话**留下的
|
||
|
||
**场景**(2026-10-29 同一套流程在同一天跑了两遍,两遍都撞上它):任务树里的文档改完、准备合回 `v2-on-ui`,
|
||
主工作树却压着一个自己从没碰过的未提交改动(两次都是 `M .dsh/skills/serial-boot-test/scripts/run_boot_test.py`,
|
||
另一个并发会话/插件留下的)。它既不能提交(不是我的改动,也不知道写没写完),也不能
|
||
stash / checkout / reset / 删除(项目级 Never:丢弃别人的工作)。`$TW done <ID>` 的前置正好卡在这——
|
||
它要求主工作树干净,否则未提交改动会被卷进合并(见 C 第 6 步),于是**合并与回收一起被挡**。
|
||
|
||
**正解 = 取证 → `merge-tree` 预演 → 主树手工 `--no-ff` 合并 → 自证外部文件没被动过 → 照常收工**,六步:
|
||
|
||
1. **先取证再动手**(别一看见脏就下「谁弄坏的」结论)。记下它的 mtime 与 diff 内容,判断与本合并有无交集,
|
||
并把**合并前**的 blob id 与 `diff --stat` 存进 `ci-artifacts/`(第 4 步拿它对账):
|
||
|
||
```bash
|
||
MAIN="D:/Code/DTU Code/STM32F4-Base"
|
||
EXT=".dsh/skills/serial-boot-test/scripts/run_boot_test.py" # 外部脏文件的仓库相对路径
|
||
mkdir -p ci-artifacts
|
||
git -C "$MAIN" status --porcelain # 全量脏清单(本会话只认自己那几条)
|
||
stat -c "%y %s %n" "$MAIN/$EXT" # mtime + 字节数:定性是谁在什么时候改的
|
||
git -C "$MAIN" diff -- "$EXT" | cat # 内容性质;与本次合并有无同文件同区域
|
||
git -C "$MAIN" hash-object "$MAIN/$EXT" | tee ci-artifacts/ext-blob-before.txt # ← 存下这个 blob id
|
||
git -C "$MAIN" diff --stat -- "$EXT" | tee ci-artifacts/ext-stat-before.txt # ← 以及这份统计(几加几减)
|
||
```
|
||
|
||
实测:mtime 落在另一会话的活跃时段内,diff 是「docstring 引号改写」2+/2−,与要合的文档毫无交集 →
|
||
定性为**与本合并无交集、不丢弃**。顺手 `git -C "$MAIN" worktree list` 看一眼有没有**别人中途新建的任务树**
|
||
(两次里都撞见第二棵树 `data-trust-restructure`,正是这份外部改动的来路)——这是并发会话的硬证据。
|
||
2. **`merge-tree` 预演:先证明能干净合并,再碰主干**。`$TW done --dry-run` 在这条路上同样被前置挡住,
|
||
预演只能自己敲(git 2.38+ 的新语法,打印的是**结果树 sha**):
|
||
|
||
```bash
|
||
git -C "$MAIN" merge-tree --write-tree v2-on-ui task/<ID>-<slug>; echo MT_RC=$?
|
||
```
|
||
|
||
判据 = **打印出一个 tree sha 且 RC=0**(实测打出 `4220906b…` + `MT_RC=0` 才敢往下走)。⚠ 本机 git 太旧没有
|
||
这个子命令时退回 `$TW done <ID> --dry-run`——它的前置也是「主树干净」,可用环境变量 `DTU_INTEGRATION_WORKTREE`
|
||
把集成点指到一棵干净的临时树去演练(见本文开头的环境变量);**别为了跑 dry-run 去动主树那份脏改动**。
|
||
预演报冲突 → 回到冲突本身处理,不许直接 `--no-ff` 硬合。
|
||
3. **主工作树里手工 `--no-ff` 合并**(合并信息走 `-F` 文件,放在被忽略的 `ci-artifacts/`;编码口径同 C 第 5 步:
|
||
UTF-8 无 BOM、正文每行 ≤80 字符;标题格式 `merge: task/<ID>-<slug> 合入 <base>`):
|
||
|
||
```bash
|
||
git -C "$MAIN" merge --no-ff --no-edit -F ci-artifacts/merge-msg-<slug>.txt task/<ID>-<slug>
|
||
```
|
||
|
||
pre-commit 会**自己**把那份未提交改动 stash 起来、跑完钩子再放回(输出里能看到 `[INFO] Stashing unstaged files to …`)
|
||
——**这条不是让你去手工 stash**,正好相反:钩子的 stash 往返让它自己走完,你只需在第 4 步验证它把东西原样还回来了。
|
||
**为什么可以绕开 `done`**(报告里要写清这句):`done` 的前置要求主工作树干净,而那份外部改动既不能提交也不能
|
||
丢弃,所以合并只剩「手工预演 + 手工合并」一条合法路径;被跳过的门禁与审计由第 2 步(预演 sha + RC=0)、
|
||
第 4 步(blob id 对账)、G 节(合并后在主树跑全库基线)、第 6 步(`lock status`)逐项补回。
|
||
**这不是绕过保护**,是把保护要求的证据逐条自己交出来。
|
||
4. **自证那份外部改动原样还在**(本节唯一的硬证据,两次实测都靠它结案):合并后**再跑一次**同一对命令,
|
||
与第 1 步存的两个值**逐项相同**:
|
||
|
||
```bash
|
||
git -C "$MAIN" hash-object "$MAIN/$EXT" | tee ci-artifacts/ext-blob-after.txt
|
||
git -C "$MAIN" diff --stat -- "$EXT" | tee ci-artifacts/ext-stat-after.txt
|
||
diff ci-artifacts/ext-blob-before.txt ci-artifacts/ext-blob-after.txt # 空输出才算过
|
||
git -C "$MAIN" status --porcelain # 该文件仍是 M:没被顺手提交,也没被清成干净
|
||
```
|
||
|
||
实测:合并前后 blob id 同为 `369380ca8eb024b593d872455b6a020797abcd8d`、`diff --stat` 同为 2+/2−
|
||
→ 证明 pre-commit 的 stash 往返没动别人的工作;合进主干的那份文档另按 G 节复核(零残留 + 全库基线)。
|
||
blob id 变了 = stash pop 出了半冲突(现场会留下 stash 与冲突标记)→ **立刻停止收工**,把两份 blob id 与两份
|
||
diff 原样贴进报告交给该文件属主;**绝不用 `git checkout -- $EXT` 抹平**(那是销毁别人的工作,项目级 Never)。
|
||
5. **收工仍走技能自身的代码路径,别自己手写 rm**:先把台账**备份成 `*.state-bak`**(后缀与 E 第 6 步统一),
|
||
再让脚本自己摘联接:
|
||
|
||
```bash
|
||
cp "$WTROOT/.state/<ID>.json" "$MAIN/ci-artifacts/<ID>.json.state-bak" # 先备份再动
|
||
"$PY" "$TW" done <ID> # 打「已合入 <base>,跳过合并,直接清场」→ 先摘 .venv 与 simulator/SDL2 联接 → 删工作树 → 删分支 → unlink 台账
|
||
```
|
||
|
||
实测这条路径能走完(日志:`state backup -> ci-artifacts\screenlink-doc-history.json.state-bak` +
|
||
`[ok] 已摘除联接 .venv` + `[ok] 已摘除联接 simulator/SDL2`)。**若它仍拿「主工作树不干净」挡在合并前**(没走到
|
||
清场分支),改走 **E 节**的手工回收路径:台账仍按本步先 `cp` 成 `.state-bak`;第 1~2 步的前置换成
|
||
「`status --porcelain -uall` 为空 + 唯一提交为空 + `merge-base --is-ancestor` 判 MERGED」三条(本次实测三条齐),
|
||
第 3 步的联接预扫 + `rmdir` 摘联接**一步不能省**;再 `git worktree remove`(三条前置齐了就不必 `--force`)、
|
||
删分支、`git worktree prune`。清完再敲一次 `done` 会得到「任务 <ID> 不存在(缺台账 …json)」(exit 2)——
|
||
**这不是失败**(台账已被 unlink;判完成以 `git log` 为准,见 C 第 7 步)。
|
||
6. **复验收工**:`git -C "$MAIN" worktree list` 只剩主树 + **别人的树**(那棵不是你的,保留并在报告里点名,
|
||
别顺手回收 → E 节);`"$PY" "$TW" lock status` 打 `build 空闲(TTL 1.0h)` + `bench 空闲(TTL 6.0h)` 才收工。
|
||
|
||
**并发会话证据必须写进报告**:主树里那份外部脏文件、中途冒出来的第二棵任务树,都要点名——是谁的、
|
||
你怎么定性的、对这次合并的影响(本例:与本合并无交集 / 不丢弃 / 不进本次提交),以及你**没动它**的证据
|
||
(blob id 前后一致)。「不能丢弃别人的工作」类问题九成是并发会话造成的,不写清就等于没取证。
|