merge: 闸收敛批四(合回闸入库)

修流程性错误:worktree-guard 的合回闸代码此前只改了主工作区未提交,
master 上 hook 仍是旧版(只拦 commit 不拦 merge),闸机制实际未生效。
八态实测通过。回执 commit=a4fba15e。
This commit is contained in:
DKChan committed 2026-10-15 22:45:18 +08:00
commit de8d28b630
1 file changed
+94 -6
+94 -6
View File
@@ -1,6 +1,8 @@
#!/usr/bin/env bash
# worktree 铁律守卫(PreToolUse,2026-09-19 台账 87 用户立规)
# 拦两路:①Edit/Write 类落在主工作区(白名单外)且主工作区检出 master/main ②Bash 在主工作区 git commit
# worktree 铁律守卫(PreToolUse,2026-09-19 台账 87 用户立规;2026-10-15 扩合回闸)
# 拦两路:①Edit/Write 类落在主工作区(白名单外)且主工作区检出 master/main
# ②Bash 在主工作区 git commit
# ③Bash 在主工作区 git merge/rebase/cherry-pick 进 master 且无 tools/run_all_gates.sh 全量回执
# 退出码 2 = 拦截(stderr 回传模型);0 = 放行;任何解析异常一律放行(守卫是兜底,不能挡死会话收口)
proj="${ZCODE_PROJECT_DIR:-$PWD}"
tmp="$(mktemp)"
@@ -71,6 +73,7 @@ elif tool == "Bash":
cmd = ti.get("command", "") or ""
toks = cmd.split()
has_commit = False
has_merge = False
for i, t in enumerate(toks):
if t.rstrip(';') != 'git':
continue
@@ -80,6 +83,9 @@ elif tool == "Bash":
if u == "commit":
has_commit = True
break
if u in ("merge", "rebase", "cherry-pick"):
has_merge = True
break
if u.startswith("-"):
if u in ("-C", "--git-dir", "-c") and j + 1 < len(toks):
j += 2
@@ -87,8 +93,6 @@ elif tool == "Bash":
j += 1
continue
break
if not has_commit:
sys.exit(0)
# 主工作区判定:cwd 在内,或命令文本里显式指到主工作区路径(不含 worktree 兄弟目录)
norm = cmd.replace("\\", "/").lower()
ref_main = re.search(r"/d/code/g-plan2(?![\w-])", norm) or re.search(r"d:/code/g-plan2(?![\w-])", norm)
@@ -97,13 +101,97 @@ elif tool == "Bash":
# 命令文本以路径形态(斜杠前缀)显式指向兄弟 worktree 且不含主区路径时放行——显式主区
# 路径引用优先走下方拦截判定(ref_main 命中即不豁免,防「主区提交夹带 worktree 提及」绕行)。
ref_wt = re.search(r"/g-plan2-[\w-]+", norm)
if ref_wt and not ref_main:
sibling = bool(ref_wt) and not ref_main
on_trunk = trunk_branch(proj) in TRUNK
in_main = (in_dir(cwd, proj) or bool(ref_main)) and on_trunk
if not in_main or sibling:
sys.exit(0)
if (in_dir(cwd, proj) or ref_main) and trunk_branch(proj) in TRUNK:
if has_commit:
sys.stderr.write(
"[worktree 铁律] 主工作区(master)禁止直接 git commit——提交在任务 worktree 分支上完成,"
"收尾在主工作区 git merge --no-ff 合回 master。\n")
sys.exit(2)
# 合回闸(2026-10-15 用户令「迭代完成前必须全量跑一遍所有测试」):merge/rebase/cherry-pick
# 进 master 前校验 tools/run_all_gates.sh 的回执——回执缺失、quick 模式、或含红项即拦。
# 起因:iter27 台账只记 probe --click 未记 --full,t17_defer_probe 红了整个迭代无人发现,
# 根因是闸清单散落三份文档彼此不一致且无机器校验(清单唯一事实源已收敛为 run_all_gates.sh)。
if has_merge:
# 回执在「跑闸的那个 worktree」里(闸在 worktree 跑、回执随 worktree 落地),
# 主工作区自己那份是上一次合回前的旧件。故按 merge 的目标分支反查 worktree 路径,
# 找不到就在所有 sibling worktree 里找「回执 commit == 分支 tip」的那一份。
def git_out(*args, **kw):
return subprocess.run(["git", "-C", proj] + list(args),
capture_output=True, text=True, timeout=kw.get("timeout", 8)).stdout.strip()
merge_branch = None
for t in toks:
u = t.rstrip(';')
if u and not u.startswith("-") and u not in ("git", "merge", "rebase", "cherry-pick"):
merge_branch = u
break
tip = git_out("rev-parse", merge_branch) if merge_branch else ""
if not tip:
tip = git_out("rev-parse", "HEAD")
cands = []
if merge_branch:
try:
wl = git_out("worktree", "list", "--porcelain")
blocks = [b for b in wl.split("\n\n") if b.strip()]
for b in blocks:
if ("branch refs/heads/%s" % merge_branch) in b:
head_line = [l for l in b.split("\n") if l.startswith("worktree ")]
if head_line:
cands.append(os.path.abspath(winpath(head_line[0][9:])))
except Exception:
pass
# 兜底:主区 + 所有 sibling worktree
cands.append(proj)
parent = os.path.dirname(proj)
try:
for name in os.listdir(parent):
p = os.path.join(parent, name)
if os.path.isdir(os.path.join(p, ".git")) or os.path.isfile(os.path.join(p, ".git")):
cands.append(os.path.abspath(p))
except Exception:
pass
receipt = None
for base in cands:
p = os.path.join(base, "game", "tests", ".gates", "receipt.json")
if not os.path.exists(p):
continue
try:
import json
with open(p, encoding="utf-8") as f:
r = json.load(f)
except Exception:
continue
# 只认「回执 commit == 将要合入的分支 tip」的那一份
if r.get("commit") == tip:
receipt = (base, r)
break
why = None
if receipt is None:
found = any(os.path.exists(os.path.join(b, "game", "tests", ".gates", "receipt.json"))
for b in cands)
why = ("无与分支 %s(tip %s)匹配的回执%s"
% (merge_branch or "?", tip[:8],
"(worktree 内存在回执但 commit 对不上=闸跑完后代码又变了)" if found else ""))
else:
base, r = receipt
if r.get("quick_mode"):
why = "回执是 --quick 模式(跳过渲染闸/静态扫描),收口出库必须跑全量"
elif not r.get("all_green"):
reds = [g["name"] for g in r.get("gates", []) if g.get("exit") != 0]
why = "回执含新增红项:" + ("、".join(reds) if reds else "见 receipt.json")
if why:
sys.stderr.write(
"[合回闸] 拦截 %s 进 master——%s。\n"
"在任务 worktree 内跑全量闸:tools/run_all_gates.sh(不带 --quick),"
"跑完回执自动写入该 worktree 的 game/tests/.gates/receipt.json,再合。\n"
% (cmd.split()[0], why))
sys.exit(2)
sys.exit(0)
PY