Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions templates/scripts/agent-file-locks.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,8 +210,13 @@ def staged_changes(repo_root: Path) -> list[tuple[str, str]]:


def list_worktree_roots(repo_root: Path) -> list[Path]:
"""Every worktree path for this repo (porcelain). Falls back to [repo_root]
when git cannot enumerate, so single-checkout behavior is unchanged."""
"""Every worktree path for this repo (porcelain), ALWAYS including the
caller's own repo_root. Submodules checked out inside a LINKED worktree
report their gitdir (<parent>/.git/worktrees/<wt>/modules/<sub>) as the
worktree path, not the actual working tree — repo_root (resolved via
--show-toplevel) is where claims are written, so it must be in the list
or claim/validate/status never see this checkout's own lock file. Falls
back to [repo_root] when git cannot enumerate."""
try:
out = run_git(['worktree', 'list', '--porcelain'], cwd=repo_root)
except LockError:
Expand All @@ -221,7 +226,9 @@ def list_worktree_roots(repo_root: Path) -> list[Path]:
for line in out.splitlines():
if line.startswith('worktree '):
roots.append(Path(line[len('worktree '):].strip()).resolve())
return roots or [repo_root]
if repo_root not in roots:
roots.append(repo_root)
return roots


def load_all_locks(repo_root: Path) -> dict[str, list[dict[str, Any]]]:
Expand Down
30 changes: 30 additions & 0 deletions test/agent-file-locks.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,36 @@ defineSpawnSuite('agent-file-locks cross-worktree (G2)', () => {
assert.match(v1.stderr, /another owner/);
});

test('claims in a submodule under a LINKED worktree are visible to validate (gitdir-root quirk)', () => {
// A submodule checked out inside a linked worktree reports its gitdir
// (<parent>/.git/worktrees/<wt>/modules/<sub>) as the worktree path in
// `git worktree list`, so load_all_locks used to miss the REAL working
// tree's lock file — validate rejected claims it had just recorded.
const subSrc = makeRepo();
const parent = makeRepo();
assert.equal(
runHumanCmd('git', ['-c', 'protocol.file.allow=always', 'submodule', 'add', subSrc, 'sub'], parent).status,
0,
'submodule add must succeed',
);
assert.equal(runHumanCmd('git', ['commit', '-m', 'add submodule'], parent).status, 0);

const wtSub = path.join(parent, '..', 'wt-sub');
assert.equal(runHumanCmd('git', ['worktree', 'add', '-q', '-b', 'agent/sub/lane', wtSub], parent).status, 0);
assert.equal(
runHumanCmd('git', ['-c', 'protocol.file.allow=always', 'submodule', 'update', '--init'], wtSub).status,
0,
'submodule init inside the linked worktree must succeed',
);

const subWt = path.join(wtSub, 'sub');
writeFile(subWt, 'sub-file.txt');
assert.equal(locksAt(subWt, ['claim', '--branch', 'agent/sub/lane', 'sub-file.txt']).status, 0);

const v = locksAt(subWt, ['validate', '--branch', 'agent/sub/lane', 'sub-file.txt']);
assert.equal(v.status, 0, `validate must see the claim it just wrote: ${v.stderr}`);
});

test('a lane can still claim + commit a file no other worktree owns', () => {
const repoDir = makeRepo();
writeFile(repoDir, 'mine.txt');
Expand Down
Loading