feat(session): worktree 铁律批(台账 87,2026-09-19 用户立规:一切开发/设计任务走独立 worktree,完事合并回 master,主工作区禁止直改直提交)——①CLAUDE.md 新增「分支纪律(worktree 铁律)」节+硬性红线增首条:开工建 ../g-plan2-<任务名> worktree、收尾 merge --no-ff 合回+清 worktree、豁免面仅 production/session-state/ 与 .zcode/(会话收口与钩子自举);②PreToolUse 钩子 .zcode/hooks/worktree-guard.sh 双路拦截:Edit/Write/MultiEdit 落主工作区(白名单外)且主区检出 master/main → exit 2;Bash 在主区 git commit(含 -C/cd 指向主区、链式命令;路径边界正则防 worktree 兄弟目录误伤)→ exit 2;解析异常一律放行(守卫是兜底不挡收口);Windows/MSYS 双路径形态归一(cygpath,修复 /d/... 被 abspath 解析成 D:\d\... 的归属失配);③config.json 注册 PreToolUse(matcher 五工具)。验证:15 用例三态测试全 PASS(主区拦/worktree 放/白名单放/-C 边界/链式 commit/merge 放行/MSYS+Windows+正斜杠路径形态);零游戏代码冒烟不适用。
This commit is contained in:
1 parent
2ca3e20d73
commit
86d341c6d7
3 files changed
+130
No files matched your search
@@ -0,0 +1,105 @@
|
||||
#!/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, "production", "session-state"))),
|
||||
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 合回。豁免面仅 production/session-state/ 与 .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
|
||||
Reference in new issue
Block a user