2727
2828import json
2929import os
30- import re
3130import shlex
3231import sys
3332from pathlib import Path
@@ -54,19 +53,6 @@ def _deny(reason: str) -> None:
5453 sys .exit (0 )
5554
5655
57- def _mind_root (command : str , cwd : str ) -> Path | None :
58- """Best-effort resolution of the PyAutoMind checkout this command targets."""
59- m = re .search (r"(?:-C\s+|cd\s+)(\S*PyAutoMind)\b" , command )
60- if m :
61- return Path (os .path .expanduser (m .group (1 )))
62- if cwd and MIND_MARKER in cwd :
63- p = Path (cwd )
64- while p .name != MIND_MARKER and p != p .parent :
65- p = p .parent
66- return p if p .name == MIND_MARKER else None
67- return None
68-
69-
7056def _clauses (command : str ):
7157 """Token-level clause split that respects quoting (v1.1).
7258
@@ -94,25 +80,72 @@ def _clauses(command: str):
9480 yield clause
9581
9682
83+ def _under_mind (path : Path ) -> Path | None :
84+ """If ``path`` is inside a PyAutoMind checkout, return that checkout root;
85+ else None."""
86+ p = path
87+ while True :
88+ if p .name == MIND_MARKER :
89+ return p
90+ if p == p .parent :
91+ return None
92+ p = p .parent
93+
94+
95+ def _cd_target (tokens : list [str ], cur : Path | None ) -> Path | None :
96+ """New effective cwd after a ``cd`` clause, or ``cur`` if not a plain cd.
97+
98+ Honouring a leading ``cd`` is what fixes the v1.1 false positive: a command
99+ that ``cd``s into PyAutoBuild before committing is NOT a Mind commit, even
100+ when the session's ambient cwd (what the hook is handed) is PyAutoMind.
101+ """
102+ if not tokens or tokens [0 ] != "cd" :
103+ return cur
104+ args = [t for t in tokens [1 :] if not t .startswith ("-" )]
105+ if not args :
106+ return cur # `cd` with no path → home; unknowable, keep current
107+ dest = Path (os .path .expanduser (args [0 ]))
108+ if dest .is_absolute () or cur is None :
109+ return dest
110+ return cur / dest
111+
112+
97113def check_command (command : str , cwd : str = "" ) -> str | None :
98114 """Return a denial reason, or None to allow."""
99115 if "PYAUTO_SKIP_MIND_GUARD=1" in command :
100116 return None
101117 if "git" not in command or "commit" not in command :
102118 return None
103- # Only reason about commands that clearly target PyAutoMind.
119+ # Cheap pre-filter: a Mind commit needs PyAutoMind named in the command
120+ # (a `cd`/`git -C` path) or in the ambient cwd. If neither, nothing to do.
104121 if MIND_MARKER not in command and MIND_MARKER not in (cwd or "" ):
105122 return None
106- root = _mind_root (command , cwd )
107123
108- # Examine each clause of the (possibly compound) command at token level.
124+ effective_cwd : Path | None = Path (cwd ) if cwd else None
125+
126+ # Walk clauses in order, tracking cwd so `cd`s before a commit are honoured.
109127 for tokens in _clauses (command ):
128+ if tokens and tokens [0 ] == "cd" :
129+ effective_cwd = _cd_target (tokens , effective_cwd )
130+ continue
110131 if "git" not in tokens or "commit" not in tokens :
111132 continue
112133 if tokens .index ("git" ) > tokens .index ("commit" ):
113134 continue
114135 if "--amend" in tokens or "--dry-run" in tokens :
115136 continue
137+ # Which repo does THIS commit target? `git -C <path>` wins; else the
138+ # effective cwd. Only guard when that repo is a PyAutoMind checkout.
139+ target_dir = effective_cwd
140+ if "-C" in tokens :
141+ ci = tokens .index ("-C" )
142+ if ci + 1 < len (tokens ):
143+ cpath = Path (os .path .expanduser (tokens [ci + 1 ]))
144+ target_dir = cpath if cpath .is_absolute () or effective_cwd is None else effective_cwd / cpath
145+ mind_root = _under_mind (target_dir ) if target_dir else None
146+ if mind_root is None :
147+ continue # not a PyAutoMind commit — e.g. a PyAutoBuild worktree
148+ root = mind_root
116149 if "--" not in tokens :
117150 return (
118151 "PyAutoMind is a SHARED checkout: concurrent sessions stage into "
0 commit comments