#!/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) 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