@@ -149,11 +149,21 @@ fn plan_shell_hook_event(
149149 return HookEventPlan :: Noop ;
150150 } ;
151151 let cwd = event. cwd . as_deref ( ) . unwrap_or ( project_root) ;
152- if !crate :: hooks:: cursor_shell_command_targets_project ( command, cwd, project_root) {
152+ let hook_project_root = hook_project_root ( cwd, project_root) ;
153+ if !crate :: hooks:: cursor_shell_command_targets_project ( command, cwd, & hook_project_root) {
153154 return HookEventPlan :: Noop ;
154155 }
156+ let hook_current_branch;
157+ let current_branch = if paths_same ( & hook_project_root, project_root) {
158+ current_branch
159+ } else {
160+ hook_current_branch = crate :: branch:: current_branch ( & hook_project_root) ;
161+ hook_current_branch. as_deref ( )
162+ } ;
155163 match crate :: hooks:: cursor_shell_sync_plan_with_current_branch ( command, current_branch) {
156- crate :: hooks:: CursorShellSyncPlan :: BranchAdd ( branch) => HookEventPlan :: AddBranch ( branch) ,
164+ crate :: hooks:: CursorShellSyncPlan :: BranchAdd ( branch) => {
165+ branch_plan_for_root ( project_root, hook_project_root, branch)
166+ }
157167 crate :: hooks:: CursorShellSyncPlan :: WorktreeBranchAdd {
158168 branch,
159169 worktree_path,
@@ -165,15 +175,59 @@ fn plan_shell_hook_event(
165175 HookEventPlan :: DebouncedIncrementalSync ( event. agent )
166176 }
167177 crate :: hooks:: CursorShellSyncPlan :: CurrentBranchSync ( branch) => {
168- HookEventPlan :: SyncCurrentBranch {
169- branch,
170- agent : event. agent ,
178+ if paths_same ( & hook_project_root, project_root) {
179+ HookEventPlan :: SyncCurrentBranch {
180+ branch,
181+ agent : event. agent ,
182+ }
183+ } else {
184+ HookEventPlan :: AddBranchAt {
185+ root : hook_project_root,
186+ branch,
187+ }
171188 }
172189 }
173190 crate :: hooks:: CursorShellSyncPlan :: Noop => HookEventPlan :: Noop ,
174191 }
175192}
176193
194+ fn hook_project_root ( cwd : & Path , project_root : & Path ) -> PathBuf {
195+ if let Some ( root) = crate :: config:: discover_project_root ( cwd) {
196+ return root;
197+ }
198+ let Some ( worktree_root) = crate :: worktree:: git_worktree_root ( cwd) else {
199+ return project_root. to_path_buf ( ) ;
200+ } ;
201+ let cwd_common = crate :: worktree:: git_common_dir ( & worktree_root) ;
202+ let project_common = crate :: worktree:: git_common_dir ( project_root) ;
203+ if cwd_common. is_some ( ) && cwd_common == project_common {
204+ worktree_root
205+ } else {
206+ project_root. to_path_buf ( )
207+ }
208+ }
209+
210+ fn branch_plan_for_root (
211+ project_root : & Path ,
212+ hook_project_root : PathBuf ,
213+ branch : String ,
214+ ) -> HookEventPlan {
215+ if paths_same ( & hook_project_root, project_root) {
216+ HookEventPlan :: AddBranch ( branch)
217+ } else {
218+ HookEventPlan :: AddBranchAt {
219+ root : hook_project_root,
220+ branch,
221+ }
222+ }
223+ }
224+
225+ fn paths_same ( a : & Path , b : & Path ) -> bool {
226+ let a = a. canonicalize ( ) . unwrap_or_else ( |_| a. to_path_buf ( ) ) ;
227+ let b = b. canonicalize ( ) . unwrap_or_else ( |_| b. to_path_buf ( ) ) ;
228+ a == b
229+ }
230+
177231fn read_marker_secs ( path : & Path ) -> Option < i64 > {
178232 std:: fs:: read_to_string ( path)
179233 . ok ( ) ?
@@ -185,6 +239,7 @@ fn read_marker_secs(path: &Path) -> Option<i64> {
185239#[ cfg( test) ]
186240mod tests {
187241 use std:: path:: Path ;
242+ use std:: process:: Command ;
188243
189244 use serde_json:: json;
190245
@@ -199,6 +254,52 @@ mod tests {
199254 }
200255 }
201256
257+ fn run_git ( cwd : & Path , args : & [ & str ] ) {
258+ let output = Command :: new ( "git" )
259+ . args ( args)
260+ . current_dir ( cwd)
261+ . output ( )
262+ . unwrap_or_else ( |e| panic ! ( "git {args:?} should run: {e}" ) ) ;
263+ assert ! (
264+ output. status. success( ) ,
265+ "git {:?} failed\n stdout:\n {}\n stderr:\n {}" ,
266+ args,
267+ String :: from_utf8_lossy( & output. stdout) ,
268+ String :: from_utf8_lossy( & output. stderr)
269+ ) ;
270+ }
271+
272+ fn setup_linked_session_worktree ( ) -> ( tempfile:: TempDir , std:: path:: PathBuf , std:: path:: PathBuf )
273+ {
274+ let base = tempfile:: tempdir ( ) . unwrap_or_else ( |e| panic ! ( "tempdir should create: {e}" ) ) ;
275+ let base_root = base
276+ . path ( )
277+ . canonicalize ( )
278+ . unwrap_or_else ( |e| panic ! ( "tempdir should canonicalize: {e}" ) ) ;
279+ let project_root = base_root. join ( "project" ) ;
280+ let worktree_root = base_root. join ( "session-worktree" ) ;
281+ std:: fs:: create_dir_all ( project_root. join ( "src" ) )
282+ . unwrap_or_else ( |e| panic ! ( "project dirs should create: {e}" ) ) ;
283+ std:: fs:: write ( project_root. join ( "src/lib.rs" ) , "pub fn marker() {}\n " )
284+ . unwrap_or_else ( |e| panic ! ( "source should write: {e}" ) ) ;
285+ run_git ( & project_root, & [ "init" , "-b" , "main" ] ) ;
286+ run_git ( & project_root, & [ "config" , "user.email" , "test@test.com" ] ) ;
287+ run_git ( & project_root, & [ "config" , "user.name" , "Test" ] ) ;
288+ run_git ( & project_root, & [ "add" , "." ] ) ;
289+ run_git ( & project_root, & [ "commit" , "-m" , "initial" ] ) ;
290+ run_git (
291+ & project_root,
292+ & [
293+ "worktree" ,
294+ "add" ,
295+ worktree_root. to_str ( ) . unwrap ( ) ,
296+ "-b" ,
297+ "feature/session" ,
298+ ] ,
299+ ) ;
300+ ( base, project_root, worktree_root)
301+ }
302+
202303 #[ test]
203304 fn parses_agent_and_event_kind_from_hook_notification ( ) {
204305 let params = json ! ( {
@@ -359,6 +460,48 @@ mod tests {
359460 ) ;
360461 }
361462
463+ #[ test]
464+ fn plans_branch_switch_from_session_worktree_against_worktree_root ( ) {
465+ let ( _base, project_root, worktree_root) = setup_linked_session_worktree ( ) ;
466+
467+ let params = json ! ( {
468+ "agent" : "codex" ,
469+ "event" : "postToolUseShell" ,
470+ "command" : "git switch feature/session" ,
471+ "cwd" : worktree_root
472+ } ) ;
473+ let event = parse_or_panic ( & params) ;
474+
475+ assert_eq ! (
476+ plan_hook_event( & event, & project_root, Some ( "main" ) ) ,
477+ HookEventPlan :: AddBranchAt {
478+ root: worktree_root,
479+ branch: "feature/session" . to_string( ) ,
480+ }
481+ ) ;
482+ }
483+
484+ #[ test]
485+ fn plans_ambiguous_git_change_from_session_worktree_with_worktree_branch ( ) {
486+ let ( _base, project_root, worktree_root) = setup_linked_session_worktree ( ) ;
487+
488+ let params = json ! ( {
489+ "agent" : "codex" ,
490+ "event" : "postToolUseShell" ,
491+ "command" : "git pull --rebase" ,
492+ "cwd" : worktree_root
493+ } ) ;
494+ let event = parse_or_panic ( & params) ;
495+
496+ assert_eq ! (
497+ plan_hook_event( & event, & project_root, Some ( "main" ) ) ,
498+ HookEventPlan :: AddBranchAt {
499+ root: worktree_root,
500+ branch: "feature/session" . to_string( ) ,
501+ }
502+ ) ;
503+ }
504+
362505 #[ test]
363506 fn plans_workspace_open_as_current_branch_sync ( ) {
364507 let params = json ! ( {
0 commit comments