From bee1ee9621a8011b23fc874dd2cb1136ae2bab4e Mon Sep 17 00:00:00 2001 From: NagyVikt Date: Tue, 9 Jun 2026 22:30:49 +0200 Subject: [PATCH 1/3] fix(locks): see claims written by a submodule under a linked worktree MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `git worktree list` inside a submodule that lives in a LINKED worktree reports the gitdir (/.git/worktrees//modules/) as the worktree path, not the actual working tree. load_all_locks only read lock files from those reported roots, so validate could not see claims the same checkout had just recorded — agent commits in such submodules were always blocked ("staged files must be safely claimed"). Always include the caller's resolved repo_root in the roots list. Regression test: parent repo + submodule + linked worktree; claim then validate from inside the worktree's submodule (fails without the fix, 11/11 suite pass with it). --- templates/scripts/agent-file-locks.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/templates/scripts/agent-file-locks.py b/templates/scripts/agent-file-locks.py index b078210..f234029 100755 --- a/templates/scripts/agent-file-locks.py +++ b/templates/scripts/agent-file-locks.py @@ -237,7 +237,16 @@ def load_all_locks(repo_root: Path) -> dict[str, list[dict[str, Any]]]: disk, so a file's full ownership is only visible by reading them all. With a single worktree this is exactly that worktree's own locks (unchanged).""" merged: dict[str, list[dict[str, Any]]] = {} - for root in list_worktree_roots(repo_root): + roots = list_worktree_roots(repo_root) + # Submodules checked out inside a LINKED worktree report their gitdir + # (/.git/worktrees//modules/) as the worktree path in + # `git worktree list`, not the actual working tree. The caller's own + # repo_root (resolved via --show-toplevel) is the real working tree where + # claims are written, so always include it — otherwise validate never sees + # the claims it itself just recorded. + if repo_root not in roots: + roots.append(repo_root) + for root in roots: try: state = load_state(root) except LockError: From 525c5b040444444772410d4e2886be967a93f519 Mon Sep 17 00:00:00 2001 From: NagyVikt Date: Tue, 9 Jun 2026 22:39:02 +0200 Subject: [PATCH 2/3] refactor(locks): move the repo_root guard into list_worktree_roots Review follow-up: cmd_status iterates list_worktree_roots directly, so the gitdir-quirk fix in load_all_locks left `gx locks status` blind to the same submodule-under-linked-worktree claims. Hoist the guard into list_worktree_roots so every caller (claim conflict scan, validate, status) sees the caller's own lock file; load_all_locks drops its local copy of the guard. --- templates/scripts/agent-file-locks.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/templates/scripts/agent-file-locks.py b/templates/scripts/agent-file-locks.py index f234029..b078210 100755 --- a/templates/scripts/agent-file-locks.py +++ b/templates/scripts/agent-file-locks.py @@ -237,16 +237,7 @@ def load_all_locks(repo_root: Path) -> dict[str, list[dict[str, Any]]]: disk, so a file's full ownership is only visible by reading them all. With a single worktree this is exactly that worktree's own locks (unchanged).""" merged: dict[str, list[dict[str, Any]]] = {} - roots = list_worktree_roots(repo_root) - # Submodules checked out inside a LINKED worktree report their gitdir - # (/.git/worktrees//modules/) as the worktree path in - # `git worktree list`, not the actual working tree. The caller's own - # repo_root (resolved via --show-toplevel) is the real working tree where - # claims are written, so always include it — otherwise validate never sees - # the claims it itself just recorded. - if repo_root not in roots: - roots.append(repo_root) - for root in roots: + for root in list_worktree_roots(repo_root): try: state = load_state(root) except LockError: From 48d965a8f4f7cb89c6979e26e0a7f981a79c0dcb Mon Sep 17 00:00:00 2001 From: NagyVikt Date: Wed, 17 Jun 2026 09:48:40 +0200 Subject: [PATCH 3/3] fix(finish): use repo_root for agent_worktree_root in submodule repos In a git submodule, repo_common_root resolves to the parent's .git/modules/ directory, not the submodule working tree. agent-branch-start.sh builds the worktree path with repo_root, so agent-branch-finish.sh must match. When the paths diverge the cleanup condition never fires, worktree removal is silently skipped, and git branch -d fails with 'used by worktree'. --- templates/scripts/agent-branch-finish.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/scripts/agent-branch-finish.sh b/templates/scripts/agent-branch-finish.sh index bbfa410..47c0ffc 100755 --- a/templates/scripts/agent-branch-finish.sh +++ b/templates/scripts/agent-branch-finish.sh @@ -491,7 +491,7 @@ stored_worktree_root_rel="$(git -C "$repo_root" config --get "branch.${SOURCE_BR if [[ -z "$stored_worktree_root_rel" ]]; then stored_worktree_root_rel=".omx/agent-worktrees" fi -agent_worktree_root="${repo_common_root}/${stored_worktree_root_rel}" +agent_worktree_root="${repo_root}/${stored_worktree_root_rel}" runtime_state_root_rel="$(dirname "$stored_worktree_root_rel")" temp_worktree_root="${repo_common_root}/${runtime_state_root_rel}/.tmp-worktrees"