fix(task-worktree): 联接列改三态点名——半联树不再被说成已联,adopt 没补齐不许报就绪

同一个谎有两处,根因是「联没联上」有两套判定:列表只取样 JUNCTION_SPECS 首项
(.venv),而 adopt 填两条。于是「有 .venv、缺 simulator/SDL2」的半联树被报成已联,
adopt 遇主线缺 SDL2 走 skip、联接失败走 warn,回来照样打「现在可以跑」——宣布就绪。

改法是把判定收成一处:
- 新增 junction_states() 三态(link / real 实体内容 / absent 真缺)与 junction_label(),
  plugin_trees() 改读三态;adopt_junctions() 也改读同一函数,消除两处逻辑分叉。
- 列表按缺的那一样分别说后果:缺 .venv 会**静默**落到机器级解释器(无 fontTools,
  即 generated-fresh 假红的老根因);缺 simulator/SDL2 是模拟器当场报错(CMakeLists.txt
  的 SDL2_DIR 是裸 set,没有环境变量逃生口)。实体目录单列齐(实体SDL2),不算共享。
- adopt 补完复算一次,仍缺就打「还不能算就绪」,不再打「现在可以跑」;锁归属与
  锁命令照常打印。
- SKILL.md 同步四处措辞,并把「adopt 之前跑不了 Python」这条不准确的说法换成上面
  两种真实失败方式(一条是静默错解释器、一条是响亮报错)。

验证:scratch 仓 16 条断言全 PASS(全联 / 只摘 SDL2 那条 → 缺 SDL2 且不得再出现已联 /
实体目录不被覆盖 / unadopt 只摘联接 / 主线缺 SDL2 时 skip + 不报就绪 / 已联的 .venv
不误报 / 两样都缺 → 缺 .venv+SDL2),同一半联态上新旧对照 = 缺 SDL2 vs 已联。
py_compile 通过;doc-links --all --sections-strict 与编号门禁绿;两文件无 BOM、无
U+FFFD、纯 LF、无行尾空格。改动只落在 .dsh/,不进体积门禁指纹面,也不需要 @build。
This commit is contained in:
2026-10-29 08:18:49 +08:00
parent db5109e41d
commit 1576d7b709
2 changed files with 66 additions and 18 deletions
+11 -8
View File
@@ -447,20 +447,22 @@ ls "$WTROOT"/.state/locks/ 2>/dev/null # 若该 ID 名下还有 .lock,先按
3. 联接与锁归属由本节命令接上(下面三步)。
**这棵树是不是插件的**:`git worktree list` 里路径含 `.dsh-worktrees/` 就是。`$TW list` 现在会在台账段
下面另起一段列出它们,并标 `已联 / 未联`。**插件树不占 `MAX_CONCURRENT = 3` 名额**——回收不归本 skill,
下面另起一段列出它们,联接列**点名缺哪一样**(`已联` / `缺 .venv` / `缺 SDL2` / `齐(实体SDL2)`)。**插件树不占
`MAX_CONCURRENT = 3` 名额**——回收不归本 skill,
硬计入只会让别人那棵关不掉的树把 `new` 挡死(`new` 会打印一行「另有 N 个插件工作树不计入上限」提醒并发代价)。
三步接上(`<name>` 可带 `worktree/` 前缀,也可多段如 `refactor/logging`):
@T3@bash
"$PY" "$TW" plugin list # 一览:分支 / 脏文件数 / 联接已联·未联
"$PY" "$TW" plugin list # 一览:分支 / 脏 / 缺哪个联接(缺项另起一行说后果)
"$PY" "$TW" plugin adopt N21-dhara-isfree # 只补缺失的 .venv 与 simulator/SDL2 联接
"$PY" "$TW" plugin unadopt N21-dhara-isfree # 只摘联接(os.rmdir),绝不动目标内容与树里文件
@T3@
- **adopt 之前那棵树跑不了 Python**:`.venv` 不入库,新检出的树里没有它 → `.venv/Scripts/python.exe`
找不到解释器,`@build`、`ci_local.py`、每个 `scripts/check_*.py` 全部起跑失败。这是插件树最常见的
「莫名失败」,先 `plugin adopt` 再谈别的。
- **两条联接坏了的样子完全不同**:缺 `.venv` → `ci_local.py` 的四级解释器回退**静默**落到机器级
python(无 fontTools = `generated-fresh` 假红的老根因,AGENTS.md 记着);缺 `simulator/SDL2` →
`build_sim.bat` 当场报错(`SDL2_DIR` 是裸 `set`,没有 env 逃生口)。所以列表只说「已联」不够——
它按缺的那一样分别给后果,`adopt` 没补齐也只报「还不能算就绪」,不许说「现在可以跑」。
- **adopt 只碰联接**:已联的跳过;`simulator/SDL2` 若已是实体目录只告警不动;`.dsh-worktrees/` 之外的路径
一律 exit 2(实测 `plugin adopt ".."` 被拒)——这道闸防的是把主树或 `_wt` 目录当插件树遮蔽掉真实内容。
- **锁归属现在认得出来**:`@build` 在插件树里自持自释时,持有者名从 cwd 反推成 `pw:<name>`(多段名压平成
@@ -619,9 +621,10 @@ ls "$WTROOT"/.state/locks/ 2>/dev/null # 若该 ID 名下还有 .lock,先按
`UNIQUE-UNMERGED` 归零(或全部已备份);保留树在报告里点名给出判据(`merge-base --is-ancestor` = MERGED)。
- **E 第 6 步清悬挂台账**:该 json 已从 `_wt/.state/` 消失(`.state-bak` 在备份目录里)、`$TW list` 不再打出它、
`$TW lock status` 两把锁仍空闲;全程未对该 ID 跑 `done` / `abort`。
- **F 节桥接**:`plugin list` 的「联接」列在 `adopt` 后变 `已联`、`unadopt` 后回 `未联`;**adopt/unadopt 前后
主树 `.venv` 与树内文件都还在**(`test -f .venv/Scripts/python.txt` 恒真);`plugin adopt ".."` 必须 exit 2;
`plugin adopt` 打印的 `pw:<name>` 与 `keil_build.py` 从 cwd 反推的名字逐字相同;`done <插件树名>` 只 exit 2。
- **F 节桥接**:`plugin list` 的「联接」列按实际三态点名(`已联` / `缺 .venv` / `缺 SDL2` /
`齐(实体SDL2)`——旧实现只取样 `.venv`,半联树会说谎);**adopt/unadopt 前后主树 `.venv` 与树内文件都
还在**;主线缺 SDL2 时 `adopt` 打 `[skip]` + 「还不能算就绪」而不打「现在可以跑」;`plugin adopt ".."`
必须 exit 2;打印的 `pw:<name>` 与 `keil_build.py` 从 cwd 反推的名字逐字相同;`done <插件树名>` 只 exit 2。
- D 节归因:测量前后 `git -C "$MAIN" status --porcelain` **逐行一致**(主树全程只读);
`git worktree list` 里净室路径已消失;逐文件增量之和 **等于**观察到的涨幅(实测 +6+8+2 = +16),且每处增量
都指到具体字面量类别(裸形 「字母 T 累进数字 4」)与属主;取数走 `import check_ids` 读 `ck.stats`,且门禁脚本自身 `git diff` 为空。
@@ -442,6 +442,35 @@ def plugin_dir_for(name: str) -> Path:
return p
def junction_states(wt: Path):
"""每个共享联接的三态:link(目录联接)/ real(实体内容)/ absent(真缺)。
这是「联没联上」的**唯一**判定处——列表显示与 adopt 收尾都读它。早先列表只取样
JUNCTION_SPECS 的第一项,于是「有 .venv、缺 simulator/SDL2」的半联树被报成已联,
而 adopt 可以只成功一条(主线缺 SDL2 走 skip、联接失败走 warn)→ 说谎的正是这一列。
"""
st = {}
for rel, _desc in JUNCTION_SPECS:
link = wt / rel
if is_reparse(link):
st[rel] = "link"
elif os.path.lexists(str(link)):
st[rel] = "real"
else:
st[rel] = "absent"
return st
def junction_label(st) -> str:
"""把三态压成一列短标签:缺什么都点名,实体内容也算「齐」但不算共享。"""
tag = {rel: rel.rsplit("/", 1)[-1] for rel in st}
absent = [tag[r] for r, s in st.items() if s == "absent"]
real = [tag[r] for r, s in st.items() if s == "real"]
if absent:
return "缺 " + "+".join(absent)
return "齐(实体" + "+".join(real) + ")" if real else "已联"
def plugin_trees():
""".dsh-worktrees/worktree/ 下的工作树(事实源 = git worktree list,不读插件 manifest)。
@@ -469,7 +498,7 @@ def plugin_trees():
"path": str(p),
"branch": ent.get("branch") or "(detached)",
"dirty": dirty,
"adopted": is_reparse(p / JUNCTION_SPECS[0][0]),
"jn": junction_states(p),
})
return trees
@@ -482,25 +511,34 @@ def print_plugin_trees() -> int:
print()
print("DSH 插件工作树({} 下;**不在本 skill 台账** → 不占 {} 个名额、done/abort "
"管不到,回收走 /worktree remove|prune):".format(PLUGIN_SUBDIR, MAX_CONCURRENT))
print("{:<26} {:<34} {:>4} {:>6}".format("名称", "分支", "脏", "联接"))
print("{:<26} {:<34} {:>4} {:>16}".format("名称", "分支", "脏", "联接"))
for t in trees:
print("{:<26} {:<34} {:>4} {:>6}".format(
print("{:<26} {:<34} {:>4} {:>16}".format(
t["name"], t["branch"], "-" if t["dirty"] < 0 else str(t["dirty"]),
"已联" if t["adopted"] else "未联"))
if any(not t["adopted"] for t in trees):
print("未联的进去跑不了 Python:「plugin adopt <name>」只建共享联接,不碰树内容")
junction_label(t["jn"])))
def miss(rel):
return any(t["jn"].get(rel) == "absent" for t in trees)
if miss(".venv"):
print(" 缺 .venv:进去跑 @build / ci_local.py 会**静默**落到机器级解释器(无 "
"fontTools,generated-fresh 假红的老根因)")
if miss("simulator/SDL2"):
print(" 缺 simulator/SDL2:模拟器 build_sim.bat 直接报错——CMakeLists.txt 的 "
"SDL2_DIR 是裸 set,没有环境变量逃生口")
if miss(".venv") or miss("simulator/SDL2"):
print(" 补法:「plugin adopt <name>」——只建缺失的联接,不碰树里任何文件")
return len(trees)
def adopt_junctions(wt: Path):
"""只补**缺失**的共享联接(已联的跳过、实体目录绝不碰),返回 (已有, 本次新建)。"""
already, made = [], []
st = junction_states(wt)
for rel, desc in JUNCTION_SPECS:
link = wt / rel
if is_reparse(link):
if st[rel] == "link":
already.append(rel)
continue
if os.path.lexists(str(link)):
if st[rel] == "real":
print("[warn] {} 已是实体目录/文件(不是联接)——不动它".format(rel))
already.append(rel)
continue
@@ -537,10 +575,17 @@ def cmd_plugin(a) -> int:
print("为插件树补共享联接:{}".format(wt))
already, made = adopt_junctions(wt)
owner = plugin_lock_owner(a.name)
# 复算一次:adopt 里 skip/warn/失败都不算成功,「已有 N + 新建 M」两两相加可能对不上
# 总数——就绪判定只能按**现在的实际状态**说,否则又是把没联上的说成联上了
still = [r for r, s in junction_states(wt).items() if s == "absent"]
print()
print("[ok] 联接:已有 {} 个、本次新建 {} 个".format(len(already), len(made)))
print("该目录现在可以跑 @build / ci_local.py / @test;build 锁由 keil_build.py "
"自持自释,持有者名就是 {}".format(owner))
if still:
print("[warn] 仍缺 {}:主线里该路径不存在或联接失败——先补主线再复跑 adopt"
"(这棵树还不能算就绪)".format("、".join(still)))
else:
print("该目录现在可以跑 @build / ci_local.py / @test")
print("build 锁由 keil_build.py 自持自释,持有者名就是 {}".format(owner))
print("上台架前:$TW lock acquire bench --task {} --note <说明>".format(owner))
print("下台架后:$TW lock release bench --task {}".format(owner))
print("回收仍归插件:/worktree remove {}(本 skill 不删插件树)".format(a.name))