From 86d341c6d74f4c8124c4b2eeabe6f7489bcc8593 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Sat, 19 Sep 2026 16:41:15 +0800 Subject: [PATCH] =?UTF-8?q?feat(session):=20worktree=20=E9=93=81=E5=BE=8B?= =?UTF-8?q?=E6=89=B9=EF=BC=88=E5=8F=B0=E8=B4=A6=2087=EF=BC=8C2026-09-19=20?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E7=AB=8B=E8=A7=84=EF=BC=9A=E4=B8=80=E5=88=87?= =?UTF-8?q?=E5=BC=80=E5=8F=91/=E8=AE=BE=E8=AE=A1=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E8=B5=B0=E7=8B=AC=E7=AB=8B=20worktree=EF=BC=8C=E5=AE=8C?= =?UTF-8?q?=E4=BA=8B=E5=90=88=E5=B9=B6=E5=9B=9E=20master=EF=BC=8C=E4=B8=BB?= =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E5=8C=BA=E7=A6=81=E6=AD=A2=E7=9B=B4=E6=94=B9?= =?UTF-8?q?=E7=9B=B4=E6=8F=90=E4=BA=A4=EF=BC=89=E2=80=94=E2=80=94=E2=91=A0?= =?UTF-8?q?CLAUDE.md=20=E6=96=B0=E5=A2=9E=E3=80=8C=E5=88=86=E6=94=AF?= =?UTF-8?q?=E7=BA=AA=E5=BE=8B=EF=BC=88worktree=20=E9=93=81=E5=BE=8B?= =?UTF-8?q?=EF=BC=89=E3=80=8D=E8=8A=82=EF=BC=8B=E7=A1=AC=E6=80=A7=E7=BA=A2?= =?UTF-8?q?=E7=BA=BF=E5=A2=9E=E9=A6=96=E6=9D=A1=EF=BC=9A=E5=BC=80=E5=B7=A5?= =?UTF-8?q?=E5=BB=BA=20../g-plan2-<=E4=BB=BB=E5=8A=A1=E5=90=8D>=20worktree?= =?UTF-8?q?=E3=80=81=E6=94=B6=E5=B0=BE=20merge=20--no-ff=20=E5=90=88?= =?UTF-8?q?=E5=9B=9E=EF=BC=8B=E6=B8=85=20worktree=E3=80=81=E8=B1=81?= =?UTF-8?q?=E5=85=8D=E9=9D=A2=E4=BB=85=20production/session-state/=20?= =?UTF-8?q?=E4=B8=8E=20.zcode/=EF=BC=88=E4=BC=9A=E8=AF=9D=E6=94=B6?= =?UTF-8?q?=E5=8F=A3=E4=B8=8E=E9=92=A9=E5=AD=90=E8=87=AA=E4=B8=BE=EF=BC=89?= =?UTF-8?q?=EF=BC=9B=E2=91=A1PreToolUse=20=E9=92=A9=E5=AD=90=20.zcode/hook?= =?UTF-8?q?s/worktree-guard.sh=20=E5=8F=8C=E8=B7=AF=E6=8B=A6=E6=88=AA?= =?UTF-8?q?=EF=BC=9AEdit/Write/MultiEdit=20=E8=90=BD=E4=B8=BB=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E5=8C=BA=EF=BC=88=E7=99=BD=E5=90=8D=E5=8D=95=E5=A4=96?= =?UTF-8?q?=EF=BC=89=E4=B8=94=E4=B8=BB=E5=8C=BA=E6=A3=80=E5=87=BA=20master?= =?UTF-8?q?/main=20=E2=86=92=20exit=202=EF=BC=9BBash=20=E5=9C=A8=E4=B8=BB?= =?UTF-8?q?=E5=8C=BA=20git=20commit=EF=BC=88=E5=90=AB=20-C/cd=20=E6=8C=87?= =?UTF-8?q?=E5=90=91=E4=B8=BB=E5=8C=BA=E3=80=81=E9=93=BE=E5=BC=8F=E5=91=BD?= =?UTF-8?q?=E4=BB=A4=EF=BC=9B=E8=B7=AF=E5=BE=84=E8=BE=B9=E7=95=8C=E6=AD=A3?= =?UTF-8?q?=E5=88=99=E9=98=B2=20worktree=20=E5=85=84=E5=BC=9F=E7=9B=AE?= =?UTF-8?q?=E5=BD=95=E8=AF=AF=E4=BC=A4=EF=BC=89=E2=86=92=20exit=202?= =?UTF-8?q?=EF=BC=9B=E8=A7=A3=E6=9E=90=E5=BC=82=E5=B8=B8=E4=B8=80=E5=BE=8B?= =?UTF-8?q?=E6=94=BE=E8=A1=8C=EF=BC=88=E5=AE=88=E5=8D=AB=E6=98=AF=E5=85=9C?= =?UTF-8?q?=E5=BA=95=E4=B8=8D=E6=8C=A1=E6=94=B6=E5=8F=A3=EF=BC=89=EF=BC=9B?= =?UTF-8?q?Windows/MSYS=20=E5=8F=8C=E8=B7=AF=E5=BE=84=E5=BD=A2=E6=80=81?= =?UTF-8?q?=E5=BD=92=E4=B8=80=EF=BC=88cygpath=EF=BC=8C=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=20/d/...=20=E8=A2=AB=20abspath=20=E8=A7=A3=E6=9E=90=E6=88=90?= =?UTF-8?q?=20D:\d\...=20=E7=9A=84=E5=BD=92=E5=B1=9E=E5=A4=B1=E9=85=8D?= =?UTF-8?q?=EF=BC=89=EF=BC=9B=E2=91=A2config.json=20=E6=B3=A8=E5=86=8C=20P?= =?UTF-8?q?reToolUse=EF=BC=88matcher=20=E4=BA=94=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=EF=BC=89=E3=80=82=E9=AA=8C=E8=AF=81=EF=BC=9A15=20=E7=94=A8?= =?UTF-8?q?=E4=BE=8B=E4=B8=89=E6=80=81=E6=B5=8B=E8=AF=95=E5=85=A8=20PASS?= =?UTF-8?q?=EF=BC=88=E4=B8=BB=E5=8C=BA=E6=8B=A6/worktree=20=E6=94=BE/?= =?UTF-8?q?=E7=99=BD=E5=90=8D=E5=8D=95=E6=94=BE/-C=20=E8=BE=B9=E7=95=8C/?= =?UTF-8?q?=E9=93=BE=E5=BC=8F=20commit/merge=20=E6=94=BE=E8=A1=8C/MSYS+Win?= =?UTF-8?q?dows+=E6=AD=A3=E6=96=9C=E6=9D=A0=E8=B7=AF=E5=BE=84=E5=BD=A2?= =?UTF-8?q?=E6=80=81=EF=BC=89=EF=BC=9B=E9=9B=B6=E6=B8=B8=E6=88=8F=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E5=86=92=E7=83=9F=E4=B8=8D=E9=80=82=E7=94=A8=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .zcode/config.json | 14 +++++ .zcode/hooks/worktree-guard.sh | 105 +++++++++++++++++++++++++++++++++ CLAUDE.md | 11 ++++ 3 files changed, 130 insertions(+) create mode 100644 .zcode/hooks/worktree-guard.sh diff --git a/.zcode/config.json b/.zcode/config.json index 206d64df..23e81144 100644 --- a/.zcode/config.json +++ b/.zcode/config.json @@ -2,6 +2,20 @@ "hooks": { "enabled": true, "events": { + "PreToolUse": [ + { + "matcher": "Edit|Write|MultiEdit|NotebookEdit|Bash", + "hooks": [ + { + "type": "process", + "command": "bash", + "args": ["${ZCODE_PROJECT_DIR}/.zcode/hooks/worktree-guard.sh"], + "timeoutMs": 10000, + "statusMessage": "worktree 铁律守卫(主工作区直改/提交拦截)" + } + ] + } + ], "SessionStart": [ { "hooks": [ diff --git a/.zcode/hooks/worktree-guard.sh b/.zcode/hooks/worktree-guard.sh new file mode 100644 index 00000000..4ab5128a --- /dev/null +++ b/.zcode/hooks/worktree-guard.sh @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md index 993b63e7..1b92a407 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -45,8 +45,19 @@ | 会话进度记录 | `production/session-state/active.md`——顶部「当前状态快照」节 + 批次台账;收口先改快照再记账,每条 ≤3 行,详情外链 | | 工程路线图 | `production/roadmap.md`——里程碑时间线与下一步顺序(批级视图;实时状态看快照节) | +## 分支纪律(worktree 铁律,2026-09-19 用户立规) + +主工作区(本目录,`master` 检出)只作**合并落点与共享状态载体**,一切开发/设计改动不得直接发生在主工作区: + +1. **开工**:每个任务批次先建独立 worktree 分支再动手:`git worktree add ../g-plan2-<任务名> -b <任务分支>`。所有代码/文档/数值改动与验证跑测都在 worktree 内进行(注意:未跟踪文件不会出现在新 worktree,需手工搬运) +2. **收尾**:验证全绿(冒烟 + 场景探针)后,在主工作区 `git merge --no-ff <任务分支>` 落回 master,然后 `git worktree remove` + 删分支;合并前先看 active.md 尾部台账,避免与并行会话撞号/漏捎带 +3. **豁免面(仅这两处允许主工作区直改)**:`production/session-state/`(会话收口)与 `.zcode/`(钩子配置)——其余一律走 worktree;主工作区 `git commit` 会被钩子直接拦截 +4. 本铁律由 PreToolUse 钩子 `.zcode/hooks/worktree-guard.sh` 强制兜底;钩子只约束 agent 侧,用户本人手工改动不受限 + ## 硬性红线(勿违反) +- **worktree 铁律**:主工作区禁止直接改动与提交(细则见上节;并行会话各占 worktree 互不踩踏) + - **数值单一事实源**:玩法数值只存 `data/tables/`(.tres),经 `ConfigDB` autoload 类型化单点读取;禁止数值字面量与自建 `load()` 表路径 - **RNG 单点**:禁止 `randomize()` / 自建 RandomNumberGenerator——随机只经 `RngService` 封闭命名流 - **Core 层零 UI 依赖**:完整战斗必须可 `--headless --script` 跑通