diff --git a/.dsh/skills/task-worktree/SKILL.md b/.dsh/skills/task-worktree/SKILL.md index afd7fa6d..096f345f 100644 --- a/.dsh/skills/task-worktree/SKILL.md +++ b/.dsh/skills/task-worktree/SKILL.md @@ -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 个插件工作树不计入上限」提醒并发代价)。 三步接上(`` 可带 `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:`(多段名压平成 @@ -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:` 与 `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:` 与 `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` 为空。 diff --git a/.dsh/skills/task-worktree/scripts/task_worktree.py b/.dsh/skills/task-worktree/scripts/task_worktree.py index db060425..63b2e5e5 100644 --- a/.dsh/skills/task-worktree/scripts/task_worktree.py +++ b/.dsh/skills/task-worktree/scripts/task_worktree.py @@ -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 」只建共享联接,不碰树内容") + 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 」——只建缺失的联接,不碰树里任何文件") 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))