From 771b34f2112b3edc819e0ab7d384a19a54ed393b Mon Sep 17 00:00:00 2001 From: Jammy2211 Date: Fri, 17 Jul 2026 18:06:04 +0100 Subject: [PATCH] mind_commit_guard v1.2: honour a leading cd away from Mind (fix false positive) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v1.1 keyed is-this-a-Mind-commit off the ambient cwd the hook is handed. A command that cd s into a PyAutoBuild worktree before committing (with the session cwd still PyAutoMind from a prior command) was wrongly denied — it bit its own author committing PyAutoBuild#165 step 3. check_command now walks clauses tracking the effective cwd, resolves each commit target repo (git -C wins, else effective cwd), and guards ONLY when that repo is a PyAutoMind checkout. Dead _mind_root/re removed. Four regression tests pin the cd-away and git -C-away allows plus the cd-into / -C-into denies. Co-Authored-By: Claude Opus 4.8 --- bin/mind_commit_guard.py | 67 ++++++++++++++++++++++++--------- tests/test_mind_commit_guard.py | 33 ++++++++++++++++ 2 files changed, 83 insertions(+), 17 deletions(-) diff --git a/bin/mind_commit_guard.py b/bin/mind_commit_guard.py index 1da7ee4..3ff64fc 100644 --- a/bin/mind_commit_guard.py +++ b/bin/mind_commit_guard.py @@ -27,7 +27,6 @@ import json import os -import re import shlex import sys from pathlib import Path @@ -54,19 +53,6 @@ def _deny(reason: str) -> None: sys.exit(0) -def _mind_root(command: str, cwd: str) -> Path | None: - """Best-effort resolution of the PyAutoMind checkout this command targets.""" - m = re.search(r"(?:-C\s+|cd\s+)(\S*PyAutoMind)\b", command) - if m: - return Path(os.path.expanduser(m.group(1))) - if cwd and MIND_MARKER in cwd: - p = Path(cwd) - while p.name != MIND_MARKER and p != p.parent: - p = p.parent - return p if p.name == MIND_MARKER else None - return None - - def _clauses(command: str): """Token-level clause split that respects quoting (v1.1). @@ -94,25 +80,72 @@ def _clauses(command: str): yield clause +def _under_mind(path: Path) -> Path | None: + """If ``path`` is inside a PyAutoMind checkout, return that checkout root; + else None.""" + p = path + while True: + if p.name == MIND_MARKER: + return p + if p == p.parent: + return None + p = p.parent + + +def _cd_target(tokens: list[str], cur: Path | None) -> Path | None: + """New effective cwd after a ``cd`` clause, or ``cur`` if not a plain cd. + + Honouring a leading ``cd`` is what fixes the v1.1 false positive: a command + that ``cd``s into PyAutoBuild before committing is NOT a Mind commit, even + when the session's ambient cwd (what the hook is handed) is PyAutoMind. + """ + if not tokens or tokens[0] != "cd": + return cur + args = [t for t in tokens[1:] if not t.startswith("-")] + if not args: + return cur # `cd` with no path → home; unknowable, keep current + dest = Path(os.path.expanduser(args[0])) + if dest.is_absolute() or cur is None: + return dest + return cur / dest + + def check_command(command: str, cwd: str = "") -> str | None: """Return a denial reason, or None to allow.""" if "PYAUTO_SKIP_MIND_GUARD=1" in command: return None if "git" not in command or "commit" not in command: return None - # Only reason about commands that clearly target PyAutoMind. + # Cheap pre-filter: a Mind commit needs PyAutoMind named in the command + # (a `cd`/`git -C` path) or in the ambient cwd. If neither, nothing to do. if MIND_MARKER not in command and MIND_MARKER not in (cwd or ""): return None - root = _mind_root(command, cwd) - # Examine each clause of the (possibly compound) command at token level. + effective_cwd: Path | None = Path(cwd) if cwd else None + + # Walk clauses in order, tracking cwd so `cd`s before a commit are honoured. for tokens in _clauses(command): + if tokens and tokens[0] == "cd": + effective_cwd = _cd_target(tokens, effective_cwd) + continue if "git" not in tokens or "commit" not in tokens: continue if tokens.index("git") > tokens.index("commit"): continue if "--amend" in tokens or "--dry-run" in tokens: continue + # Which repo does THIS commit target? `git -C ` wins; else the + # effective cwd. Only guard when that repo is a PyAutoMind checkout. + target_dir = effective_cwd + if "-C" in tokens: + ci = tokens.index("-C") + if ci + 1 < len(tokens): + cpath = Path(os.path.expanduser(tokens[ci + 1])) + target_dir = cpath if cpath.is_absolute() or effective_cwd is None else effective_cwd / cpath + mind_root = _under_mind(target_dir) if target_dir else None + if mind_root is None: + continue # not a PyAutoMind commit — e.g. a PyAutoBuild worktree + root = mind_root if "--" not in tokens: return ( "PyAutoMind is a SHARED checkout: concurrent sessions stage into " diff --git a/tests/test_mind_commit_guard.py b/tests/test_mind_commit_guard.py index 703e825..3fadc21 100644 --- a/tests/test_mind_commit_guard.py +++ b/tests/test_mind_commit_guard.py @@ -111,3 +111,36 @@ def test_bare_commit_still_denied_in_compound_with_quotes(tmp_path): mind.mkdir() cmd = f'cd {mind} && git commit -q -m "msg; with semicolon" && echo ok' assert check_command(cmd) is not None + + +# --- v1.2: honour a `cd` away from Mind (false positive on a PyAutoBuild commit) --- +def test_cd_to_other_repo_before_commit_is_allowed(): + # The 2026-07-17 false positive: session cwd was PyAutoMind (what the hook + # is handed) but the command cd's to a PyAutoBuild worktree first. That is + # NOT a Mind commit — a bare `git commit` there is fine. + r = check_command( + 'cd /home/x/wt/PyAutoBuild && git add a && git commit -m "m"', + cwd="/home/x/PyAutoMind", + ) + assert r is None + + +def test_git_dash_C_to_other_repo_from_mind_cwd_is_allowed(): + r = check_command( + 'git -C /home/x/wt/PyAutoBuild commit -m "m"', cwd="/home/x/PyAutoMind" + ) + assert r is None + + +def test_cd_into_mind_then_bare_commit_still_denied(): + r = check_command( + 'cd /home/x/PyAutoMind && git commit -m "m"', cwd="/tmp/elsewhere" + ) + assert r is not None + + +def test_git_dash_C_into_mind_from_other_cwd_still_denied(): + r = check_command( + 'git -C /home/x/PyAutoMind commit -m "m"', cwd="/home/x/wt/PyAutoBuild" + ) + assert r is not None