Files
gplan-2/.zcode/hooks/worktree-guard.sh
T

113 lines
4.5 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# worktree 铁律守卫(PreToolUse,2026-09-19 台账 87 用户立规)
# 拦两路:①Edit/Write 类落在主工作区(白名单外)且主工作区检出 master/main ②Bash 在主工作区 git commit
# 退出码 2 = 拦截(stderr 回传模型);0 = 放行;任何解析异常一律放行(守卫是兜底,不能挡死会话收口)
proj="${ZCODE_PROJECT_DIR:-$PWD}"
tmp="$(mktemp)"
cat > "$tmp"
projw="$(cygpath -w "$proj" 2>/dev/null || echo "$proj")"
python - "$projw" "$(cygpath -w "$tmp" 2>/dev/null || echo "$tmp")" <<'PY'
import json, os, re, subprocess, sys
try:
sys.stderr.reconfigure(encoding='utf-8', errors='replace')
except Exception:
pass
def winpath(p):
# MSYS 风格路径(/d/...)原生 Windows Python 不识别,统一转 Windows 形式
if isinstance(p, str) and re.match(r"^/[A-Za-z](/|$)", p):
try:
r = subprocess.run(["cygpath", "-w", p], capture_output=True, text=True, timeout=5)
return r.stdout.strip() or p
except Exception:
return p
return p
proj = os.path.abspath(winpath(sys.argv[1]))
try:
with open(sys.argv[2], encoding='utf-8') as f:
d = json.load(f)
except Exception:
sys.exit(0)
tool = d.get("tool_name") or d.get("tool") or ""
ti = d.get("tool_input") or {}
cwd = os.path.abspath(winpath(d.get("cwd") or proj))
def in_dir(p, root):
p = os.path.normcase(os.path.abspath(p))
root = os.path.normcase(os.path.abspath(root))
return p == root or p.startswith(root.rstrip('\\/') + os.sep)
def trunk_branch(repo):
try:
r = subprocess.run(["git", "-C", repo, "branch", "--show-current"],
capture_output=True, text=True, timeout=5)
return r.stdout.strip()
except Exception:
return ""
TRUNK = {"master", "main"}
WHITELIST = [os.path.abspath(winpath(os.path.join(proj, "active.md"))),
os.path.abspath(winpath(os.path.join(proj, ".zcode")))]
if tool in ("Edit", "Write", "MultiEdit", "NotebookEdit"):
fp = ti.get("file_path") or ti.get("notebook_path") or ""
if not fp:
sys.exit(0)
fp = os.path.abspath(winpath(fp))
if not in_dir(fp, proj) or any(in_dir(fp, w) for w in WHITELIST):
sys.exit(0)
b = trunk_branch(proj)
if b in TRUNK:
sys.stderr.write(
"[worktree 铁律] 主工作区(%s 分支)禁止直接改动:%s\n"
"先建独立 worktree 再改:git worktree add ../g-plan2-<任务名> -b <任务分支>;"
"收尾在主工作区 git merge --no-ff 合回。豁免面仅根目录 active.md 与 .zcode/。\n" % (b, fp))
sys.exit(2)
elif tool == "Bash":
cmd = ti.get("command", "") or ""
toks = cmd.split()
has_commit = False
for i, t in enumerate(toks):
if t.rstrip(';') != 'git':
continue
j = i + 1
while j < len(toks) and j - i <= 5:
u = toks[j].rstrip(';')
if u == "commit":
has_commit = True
break
if u.startswith("-"):
if u in ("-C", "--git-dir", "-c") and j + 1 < len(toks):
j += 2
continue
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)
# 兄弟 worktree 豁免(2026-10-03 用户授权修复):harness 上报 cwd 恒为项目根,原判定把
# git -C <worktree> commit 一并误拦,与「提交在任务 worktree 分支上完成」的放行语义相抵;
# 命令文本以路径形态(斜杠前缀)显式指向兄弟 worktree 且不含主区路径时放行——显式主区
# 路径引用优先走下方拦截判定(ref_main 命中即不豁免,防「主区提交夹带 worktree 提及」绕行)。
ref_wt = re.search(r"/g-plan2-[\w-]+", norm)
if ref_wt and not ref_main:
sys.exit(0)
if (in_dir(cwd, proj) or ref_main) and trunk_branch(proj) in TRUNK:
sys.stderr.write(
"[worktree 铁律] 主工作区(master)禁止直接 git commit——提交在任务 worktree 分支上完成,"
"收尾在主工作区 git merge --no-ff 合回 master。\n")
sys.exit(2)
sys.exit(0)
PY
rc=$?
rm -f "$tmp"
exit $rc