围栏腐坏是真 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、单个尾换行。
15 KiB
E. 回收非登记工作树($TW list 为空 / detached HEAD / 树里有在飞或未跟踪内容)
这类树的判定特征(命中任意一条就适用本节):git worktree list 里有它、但 _wt/.state/<ID>.json 台账不存在;
HEAD 是 detached(git rev-parse --abbrev-ref HEAD 打 HEAD);或树里的改动不属于本会话(别的会话留下的未提交 /
未跟踪文件)。典型来源:D 节的 --detach 净室、new 之后被手工删目录只剩台账的残骸、别人手工 git worktree add 建的分析树
(2026-10-28 实测一次回收 3 棵:verify-task16 / verify-batch / und-attr,各 89 MB,全部无台账 + detached)。
为什么 done / abort 在这里整体不适用(铁律 9):两条子命令的前置都是 load_state(),无台账 →
前置/环境错误:任务 <ID> 不存在(缺台账 …json)(exit 2)当场停住、什么也不会发生;而假如有一份残留台账,
abort --force 会拿台账的 path 直接 git worktree remove —— 绕过下面第 1~4 步的全部内容保全,把别的会话的唯一版本一次删光;
done 则会去找那条(多半已不存在的)task/ 分支做合并。另:本路径没有「合并」这一步,所以 done 的并集合并残留审计
在这里不发生,也没有三方对比可依据 —— 唯一性只由第 2 步的内容分桶给出。回收只能手工走本节第 1~5 步 + 第 6 步清台账。
- 建全景(全程只读):先看
git worktree list的全集,再逐棵记三件事(脏行数含未跟踪 / 树顶提交 / 有无分支) 加一条台账判定,同时存下主树的拆前基线清单:
MAIN="D:/Code/DTU Code/STM32F4-Base"; WTROOT="$MAIN/../_wt"
git -c core.quotePath=false worktree list # 全集:登记树 + 非登记树都在这份里
MAIN_SHA=$(git -C "$MAIN" rev-parse --short HEAD) # 下面所有比对都以这一版为主线尺子
mkdir -p ci-artifacts
git -C "$MAIN" status --porcelain -uall > ci-artifacts/wt-main-before.txt # 拆前主树基线(第 5 步逐行比)
wc -l < ci-artifacts/wt-main-before.txt # 记下行数(实测 0)
for t in "$WTROOT"/*/; do
echo "--- $t"
git -c core.quotePath=false -C "$t" status --porcelain -uall | wc -l # 脏 + 未跟踪行数
git -c core.quotePath=false -C "$t" log --oneline -1 # 树顶提交
git -C "$t" rev-parse --abbrev-ref HEAD # 打 HEAD = detached,压根没分支可 done
ls "$WTROOT/.state/$(basename "$t").json" 2>/dev/null || echo "NO-LEDGER" # 台账位置随 DTU_WT_ROOT
done
-uall不是可选:默认status --porcelain会把未跟踪目录折叠成一行dir/,-uall才展开到文件级;少了它 第 2 步的分桶就漏项(实测und-attr唯一那份就是未跟踪?? scripts/check_nested_comment.py)。- 路径含空格(本项目
DTU Code)→ 变量整体加引号;core.quotePath=false让中文路径原样输出,便于逐行核对。 - 判定「是不是登记树」看两样:台账在不在 + HEAD 是否 detached。两者都指「非登记」→ 从这里开始禁止
done/abort(见本节开头),继续第 2 步。
- 逐文件与主线 HEAD 比内容并分三桶:
IN-MAINLINE(内容等价,拆 = 零损失)/UNIQUE-UNMERGED(主线没有这份 内容,拆 = 丢)/OLDER-DRAFT(同路径主线已有更新版本)。落成独立只读探针跑(别用 heredoc 现写现改, 见陷阱「别用 heredoc 打补丁造探针」):
# ci-artifacts/wt_bucket_scan.py — 只读:逐文件与主线工作区/HEAD 比对,三桶归类
import os, subprocess, tempfile
MAIN = r"D:\Code\DTU Code\STM32F4-Base"
WTROOT = os.path.join(os.path.dirname(MAIN), "_wt")
TREES = ["verify-task16", "verify-batch", "und-attr"] # ← 第 1 步全景里挑出的非登记树
SKIP_TOP = {".venv", "MDK-ARM", ".git", "ci-artifacts"} # 联接 / 构建产物 / 忽略区,按项目调
def git(*a):
return subprocess.run(("git", "-c", "core.quotePath=false", "-C", MAIN) + a,
capture_output=True, text=True, errors="replace")
def bucket(rel, tree):
p = os.path.join(tree, rel)
if not os.path.exists(p):
return "OTHER", "工作树里已不存在(多为已 staged 删除)"
tf = tempfile.NamedTemporaryFile(delete=False)
tf.write(open(p, "rb").read()); tf.close()
# 1) 与主线工作区(= 当前 HEAD 的检出内容)逐字节比
if git("diff", "--no-index", "--quiet", tf.name, rel).returncode == 0:
os.unlink(tf.name); return "IN-MAINLINE", "与主线 HEAD 逐字节相同"
# 2) 与主线历史各版比:内容等价于某个旧 commit → 只是主线又往前走了
hist = [x.strip() for x in git("log", "--format=%H", "HEAD", "--", rel).stdout.splitlines()]
for sha in hist[1:]:
old = git("show", f"{sha}:{rel}")
if old.returncode == 0 and old.stdout == open(p, encoding="utf-8", errors="replace").read():
os.unlink(tf.name); return "IN-MAINLINE", f"内容等价于主线历史版 {sha[:8]}"
# 3) 主线从没这条路径 → 唯一;否则同路径主线有更新版 → 更旧草稿(再用 blob diff 定性方向)
os.unlink(tf.name)
if not hist:
return "UNIQUE-UNMERGED", "主线 HEAD 与历史都没有此路径"
return "OLDER-DRAFT", "同路径主线有更新版本(用 diff --stat <HEAD> -- <f> 定性)"
for name in TREES:
tree = os.path.join(WTROOT, name)
lines = subprocess.run(("git", "-c", "core.quotePath=false", "-C", tree,
"status", "--porcelain", "-uall"),
capture_output=True, text=True, errors="replace").stdout.splitlines()
tally = {}
for line in lines:
rel = line[3:].split(" -> ")[-1].strip('"') # 兼容 ?? / M / A / R 的箭头形态
if rel.split("/")[0] in SKIP_TOP:
continue
b, why = bucket(rel, tree)
tally.setdefault(b, []).append((rel, why))
g = lambda k: len(tally.get(k, []))
print(f"== {name}: IN-MAINLINE={g('IN-MAINLINE')} UNIQUE-UNMERGED={g('UNIQUE-UNMERGED')} "
f"OLDER-DRAFT={g('OLDER-DRAFT')} OTHER={g('OTHER')}")
for b in ("UNIQUE-UNMERGED", "OLDER-DRAFT", "OTHER"):
for rel, why in tally.get(b, []):
print(f" {b}: {rel} # {why}")
报告里必须逐桶给计数 + 判定证据(实测:verify-task16 IN-MAINLINE=6 / UNIQUE=0;verify-batch IN-MAINLINE=35 /
UNIQUE=1;und-attr IN-MAINLINE=0 / UNIQUE=1)。两个非空桶再各钉一条命令把证据坐实:
# 「唯一未合并」是真更新还是更旧草稿?blob 级 diff + 逐行看方向
git -c core.quotePath=false -C "$MAIN" diff --stat "$MAIN_SHA" -- simulator/tests/README.md
diff <(git -C "$MAIN" show "$MAIN_SHA":simulator/tests/README.md) "$WTROOT/verify-batch/simulator/tests/README.md"
# 「树顶提交要不要保」只看一件事:是否已并入 HEAD。别拿「目录很久没动」当判据
for c in 77779d49 1d4b6a17; do
git -C "$MAIN" merge-base --is-ancestor "$c" HEAD && echo "$c MERGED" || echo "$c NOT-merged"
done
实测:那份 simulator/tests/README.md 树内写「27 页 × 3200 B」、主线 HEAD 已是「60 张 × 3200 B」→ 更旧草稿(被取代的
早先版本),不是未合并成果;bench-21-77779d49 / bench-23-1d4b6a17 两棵 merge-base --is-ancestor 判 MERGED 且脏数 = 0
→ 保留不动(不属本节,等属主自己收,报告里点名说清为什么留)。
回收判据不是「看着旧」:唯一内容如果没被提交过,git log 里根本看不见它;反过来「提交已 MERGED」也不等于可以拆——
还得看未跟踪那一层。UNIQUE-UNMERGED 非空又确实要留 → 它进第 4 步的备份,不许靠 --force 顺手带走。
3. 拆前扫联接 / reparse point(物理安全性 = 铁律 5 的可执行判据)。存成 ci-artifacts/wt_junction_scan.py 再跑:
# 只读预扫:一个路径是不是 reparse point(Windows 目录联接在这里现形);发现即停止深入,绝不 walk 进联接
import os, ctypes
FILE_ATTRIBUTE_REPARSE_POINT = 0x400
GA = ctypes.windll.kernel32.GetFileAttributesW
def is_reparse(p):
a = GA(p)
return a != -1 and bool(a & FILE_ATTRIBUTE_REPARSE_POINT)
for name in ["verify-task16", "verify-batch", "und-attr", "bench-21-77779d49", "bench-23-1d4b6a17"]:
top = os.path.join(r"D:\Code\DTU Code\_wt", name)
hits, entries = [], 0
for root, ds, fs in os.walk(top):
bad = [d for d in ds if is_reparse(os.path.join(root, d))]
if bad:
hits += [os.path.join(root, d) + "/" for d in bad]
ds[:] = [] # 有联接就不再深入:防转进主树 .venv 把别人整棵树数一遍
hits += [os.path.join(root, f) for f in fs if is_reparse(os.path.join(root, f))]
entries += len(ds) + len(fs)
print(f"== {name} scanned entries={entries}")
print(" REPARSE: none → 可直接 git worktree remove --force"
if not hits else " REPARSE:\n " + "
".join(hits))
实测输出 == verify-task16 ... REPARSE: none(每棵的体积 / 条目数 + 零联接就是证据),而 bench-* 两棵各挂一个 .venv
联接 → 判 KEEP(本节不拆它们;将来真要拆,先 rmdir 摘掉联接再 remove)。
判据是「扫出来一个都没有」,不是「我记得没建过联接」:new --no-bootstrap 的树、别的手工 mklink /J 的树都可能带着指向主树的联接。
摘联接用 rmdir(只删联接本身;不是 rd /s,更不是 rm -rf——那会顺着联接删目标)。
非 Windows 环境改用 os.path.islink / find <tree> -type l(本项目是 Windows + Keil 工具链,按上面这条走)。
到这里才算「拆前唯一性自检 + 物理安全预扫」齐:--force 的安全前提 = 第 2 步 UNIQUE-UNMERGED 为 0
(或剩下的已全部进第 4 步备份)且这里 REPARSE: none。
4. 唯一内容先备份,再 --force 拆。UNIQUE-UNMERGED 与需要留档的 OLDER-DRAFT 一律先备份进主树
ci-artifacts/wt-leftover-backup/(ci-artifacts/ 已被 .gitignore 忽略 → 备份不会改变主树脏行数;仓库根不放一次性产物),
文件名 + 字节数必须打进输出当证据:
BK="$MAIN/ci-artifacts/wt-leftover-backup"; mkdir -p "$BK"
# 命名约定:<树名>__<原路径里的 / 换成 _>,反向可定位;台账备份统一后缀 .state-bak
cp "$WTROOT/verify-batch/simulator/tests/README.md" "$BK/verify-batch__simulator_tests_README.md"
cp "$WTROOT/und-attr/scripts/check_nested_comment.py" "$BK/und-attr__scripts_check_nested_comment.py"
ls -l "$BK" # 实测:verify-batch__simulator_tests_README.md 1769 bytes / und-attr__scripts_check_nested_comment.py 14827 bytes
for t in verify-task16 verify-batch und-attr; do
git -C "$MAIN" worktree remove --force "$WTROOT/$t"; echo "$t remove_rc=$?" # 不接管道才读得到码(见「读输出」)
done
git -C "$MAIN" worktree prune # 顺带清 .git/worktrees 里的残籍
自检:备份条目数 = 第 2 步里判「要留档」的条目数,且每个都带字节数。这一步就是本节相对 D 第 4 步的增量——
D 的前提是「净室里只有自己的拷贝」,本节的树里混着别人在飞的东西,--force 是一次不可逆删除,先备份才谈得上安全。
5. 复验:主树一个字节没动 + 树确实没了:
git -C "$MAIN" status --porcelain -uall > ci-artifacts/wt-main-after.txt
wc -l < ci-artifacts/wt-main-before.txt; wc -l < ci-artifacts/wt-main-after.txt # 两个数必须相同(实测 0 → 0)
diff ci-artifacts/wt-main-before.txt ci-artifacts/wt-main-after.txt # 空输出 = 逐行一致
git -c core.quotePath=false -C "$MAIN" worktree list # 已拆路径逐条消失;主树与判「保留」的树原样在列
行数不同 = 备份或摘除碰到了主线(多半是备份写进了未被忽略的目录,或漏摘联接)→ 立刻定位,别报「回收完成」。
第 2 步判「保留不动」的树还在 worktree list 里是预期结果,在报告里点名解释为什么留(附 MERGED 判据)。
6. 顺手清悬挂台账(branch / path 都已不存在的 _wt/.state/<ID>.json;它会让 $TW list 打出一行怪条目、并让下次
new 同 ID 报「任务 已存在」)。先给证据、后动手:
for j in "$WTROOT"/.state/*.json; do # 台账位置随 DTU_WT_ROOT
id=$(basename "$j" .json)
br=$("$PY" -c "import json,sys;print(json.load(open(sys.argv[1],encoding='utf-8')).get('branch',''))" "$j")
pp=$("$PY" -c "import json,sys;print(json.load(open(sys.argv[1],encoding='utf-8')).get('path',''))" "$j")
git -C "$MAIN" rev-parse --verify "$br" >/dev/null 2>&1 && b=branch-OK || b=branch-MISSING
[ -d "$pp" ] && p=path-OK || p=path-MISSING
echo "$id $b $p <- $j"
done
实测这条判据打出的正是 v5tool2 branch-MISSING path-MISSING:git rev-parse task/v5tool2-v11 报
fatal: ambiguous argument 'task/v5tool2-v11': unknown revision ...,台账 path 指的目录已无,git worktree list 里也没有这棵树。
三条齐(rev-parse fatal + path 目录不存在 + worktree list 无此树)才叫悬挂;缺任何一条都回到第 1~2 步查是不是真有东西要保。
然后备份 → 删 → 复跑两条 list 复验:
cp "$WTROOT/.state/v5tool2.json" "$BK/v5tool2.json.state-bak" && rm "$WTROOT/.state/v5tool2.json"
"$PY" "$TW" list # 该条目消失(全空时打「(没有任务空间。用 new <ID> <slug> 建一个)」),echo RC=$? 应为 0
"$PY" "$TW" lock status # build 空闲(TTL 1.0h)/ bench 空闲(TTL 6.0h)—— 证明清台账没牵连两把锁
ls "$WTROOT"/.state/locks/ 2>/dev/null # 若该 ID 名下还有 .lock,先按 B 节释放,再删台账
- 必须
cp先备份再rm:台账里存着 base / junctions / needs_bench / bench_pass,是真出纠纷时唯一的现场记录。 - 悬挂条目按定义
path已不存在 → 通常不占MAX_CONCURRENT = 8名额(A 第 1 步的「活跃」判据要求 path 目录还在), 但它把$TW list搅成怪输出、并永久挡住同 ID 复用;path 还在的占位/残留台账才占名额(早期为绕 B 节那个坑手写的占位台账就是这种)。两种都要清。 - 仍不对这个 ID 跑
done/abort:清场会拿台账里的死path/ 死branch去做git worktree remove和删分支 (这正是 A 第 5 步说的「手工删目录留下的尾巴」,正解就是本步)。历史遗留的占位台账同理——活着时也别done/abort,用完直接删文件(来历与现状见 B 末条)。