From a5efb7c676b01f39ea8c2a68960b951ef5eeb64b Mon Sep 17 00:00:00 2001 From: Dai Ha Date: Sun, 23 Aug 2026 20:06:36 +0200 Subject: [PATCH] CB-634: write the overlay exclude to the common git dir, not the per-worktree gitdir git reads info/exclude from the common dir for a linked worktree (only info/sparse-checkout is per-worktree), so the entry written into /worktrees//info/exclude was never honoured and CLAUDE.local.md showed as untracked -- at risk of being swept into a worker's PR. Derive the common dir (/worktrees/ -> ) and write there. Found by dogfooding a real spawn on fleet01; the test now uses the real worktree layout and asserts the entry lands in the common dir, not the per-worktree gitdir. --- .../ltms/fleet/member/ClaudeCodeLauncher.java | 14 +++++++++-- .../fleet/member/ClaudeCodeLauncherTest.java | 23 +++++++++++-------- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/bridged/src/main/java/dev/ltms/fleet/member/ClaudeCodeLauncher.java b/bridged/src/main/java/dev/ltms/fleet/member/ClaudeCodeLauncher.java index bd5ab4a..e6103f8 100644 --- a/bridged/src/main/java/dev/ltms/fleet/member/ClaudeCodeLauncher.java +++ b/bridged/src/main/java/dev/ltms/fleet/member/ClaudeCodeLauncher.java @@ -356,7 +356,8 @@ public final class ClaudeCodeLauncher extends HerdrPeerLauncher { /** * Deliver the shared IDE guidance ({@link PeerLauncher#ideOverlayText}) as an on-disk * {@code CLAUDE.local.md} overlay beside the project's own {@code CLAUDE.md} (CB-634), and - * register the overlay in the worktree's {@code info/exclude} so it never shows as untracked. + * register the overlay in the repository's common {@code info/exclude} so it never shows as + * untracked (git reads a worktree's excludes from the common dir, not the per-worktree gitdir). * *

Safety gate: the overlay is written ONLY when {@code cwd/.git} is a * regular file — a provisioned worktree keeps a {@code .git} FILE holding a @@ -382,7 +383,16 @@ public final class ClaudeCodeLauncher extends HerdrPeerLauncher { if (!gitDir.isAbsolute()) { gitDir = Path.of(cwd).resolve(gitDir).normalize(); } - Path exclude = gitDir.resolve("info").resolve("exclude"); + // git reads info/exclude from the COMMON dir, never the per-worktree gitdir (only + // info/sparse-checkout is per-worktree). A provisioned worktree's gitdir is + // /worktrees/, so the common dir is two levels up; writing the entry into + // the per-worktree gitdir leaves it un-honoured and the overlay shows as untracked. + Path commonDir = gitDir; + if (gitDir.getParent() != null && gitDir.getParent().getFileName() != null + && "worktrees".equals(gitDir.getParent().getFileName().toString())) { + commonDir = gitDir.getParent().getParent(); + } + Path exclude = commonDir.resolve("info").resolve("exclude"); Files.createDirectories(exclude.getParent()); String overlayLine = "CLAUDE.local.md"; if (!Files.exists(exclude) || Files.readAllLines(exclude).stream().noneMatch(overlayLine::equals)) { diff --git a/bridged/src/test/java/dev/ltms/fleet/member/ClaudeCodeLauncherTest.java b/bridged/src/test/java/dev/ltms/fleet/member/ClaudeCodeLauncherTest.java index 1d0e257..2e802ac 100644 --- a/bridged/src/test/java/dev/ltms/fleet/member/ClaudeCodeLauncherTest.java +++ b/bridged/src/test/java/dev/ltms/fleet/member/ClaudeCodeLauncherTest.java @@ -138,14 +138,17 @@ class ClaudeCodeLauncherTest { } // CB-634: the IDE guidance is delivered as a CLAUDE.local.md overlay (written only into a - // provisioned worktree — cwd with a `.git` FILE) and registered in the worktree's info/exclude. + // provisioned worktree — cwd with a `.git` FILE) and registered in the repository's COMMON + // info/exclude. git reads a worktree's excludes from the common dir, not the per-worktree + // gitdir (only info/sparse-checkout is per-worktree), so a real provisioned layout + // /worktrees/ must land the entry in /info/exclude. @Test - void writeIdeOverlayWritesClaudeLocalAndAddsItToInfoExclude(@TempDir Path root) throws Exception { + void writeIdeOverlayWritesClaudeLocalAndAddsItToCommonInfoExclude(@TempDir Path root) throws Exception { Path worktree = Files.createDirectory(root.resolve("worktree")); - Path gitDir = Files.createDirectory(root.resolve("gitdir")); - Files.createDirectories(gitDir.resolve("info")); - // A provisioned worktree keeps its gitdir as a `.git` FILE holding a gitdir: pointer. + // Real worktree layout: the .git FILE points at /worktrees/. + Path commonDir = Files.createDirectory(root.resolve("dotgit")); + Path gitDir = Files.createDirectories(commonDir.resolve("worktrees").resolve("wt1")); Files.writeString(worktree.resolve(".git"), "gitdir: " + gitDir); FakeHerdr herdr = new FakeHerdr(); @@ -156,10 +159,12 @@ class ClaudeCodeLauncherTest { assertTrue(Files.exists(overlay), "the overlay is written beside the project's CLAUDE.md"); assertTrue(Files.readString(overlay).contains("project_path: \"" + worktree + "\""), "the overlay pins every ide_* call to the member's own worktree"); - Path exclude = gitDir.resolve("info").resolve("exclude"); - assertTrue(Files.exists(exclude), "info/exclude is created from the gitdir pointer"); - assertTrue(Files.readAllLines(exclude).contains("CLAUDE.local.md"), - "the overlay is registered so it never shows as untracked"); + Path commonExclude = commonDir.resolve("info").resolve("exclude"); + assertTrue(Files.exists(commonExclude), "info/exclude is created in the COMMON git dir"); + assertTrue(Files.readAllLines(commonExclude).contains("CLAUDE.local.md"), + "the overlay is registered in the common exclude git actually honours for a worktree"); + assertFalse(Files.exists(gitDir.resolve("info").resolve("exclude")), + "the entry must NOT go to the per-worktree gitdir, which git ignores for excludes"); } @Test