Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package lazyworktree for openSUSE:Factory 
checked in at 2026-04-30 20:30:10
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/lazyworktree (Old)
 and      /work/SRC/openSUSE:Factory/.lazyworktree.new.30200 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "lazyworktree"

Thu Apr 30 20:30:10 2026 rev:3 rq:1350141 version:1.45.1

Changes:
--------
--- /work/SRC/openSUSE:Factory/lazyworktree/lazyworktree.changes        
2026-04-02 17:44:03.994540201 +0200
+++ /work/SRC/openSUSE:Factory/.lazyworktree.new.30200/lazyworktree.changes     
2026-04-30 20:30:35.143277426 +0200
@@ -1,0 +2,52 @@
+Thu Apr 30 05:29:09 UTC 2026 - Johannes Kastl 
<[email protected]>
+
+- Update to version 1.45.1:
+  This release introduces repo-local worktree placement using the
+  new $LWT_REPO_PATH variable, adds the ability to prune merged
+  branches without active worktrees, and improves repository path
+  resolution with URL decoding.
+  * Features
+    - Added support for repo-local worktree placement via the
+      $LWT_REPO_PATH environment variable. This allows users to
+      store worktrees within the repository itself (e.g., in a
+      .worktrees folder) while automatically omitting redundant
+      repository name segments.
+    - Introduced a new configuration option, prune_stale_branches,
+      which includes merged local branches that no longer have
+      active worktrees in the prune checklist.
+    - Implemented an automated workflow to streamline the merging
+      of Dependabot pull requests.
+  * Bug Fixes
+    - Resolved an issue where repository names with URL-encoded
+      characters (such as %20) were not decoded before creating
+      directory paths (#49).
+    - Improved the identification of Claude CLI processes by
+      ignoring the exec keyword in shell command wrappers.
+    - Fixed CI configuration to ensure workflows are correctly
+      triggered when dependency files (go.mod and go.sum) are
+      updated.
+    - Updated the release workflow to verify successful release
+      creation even if GoReleaser encounters non-critical failures
+      during secondary steps like AUR pushing.
+  * Maintenance
+    - Standardized Git configuration key lookups by automatically
+      normalizing hyphens to underscores (e.g., lw.worktree-dir now
+      correctly maps to worktree_dir).
+    - Replaced the default Dependabot Go configuration with a
+      custom weekly automated dependency update and auto-merge
+      workflow.
+    - Updated documentation with new guidelines for documentation
+      reviews and agent contribution requirements.
+  * Dependencies
+    - Updated project dependencies to their latest versions,
+      including several Charm libraries (bubbletea, lipgloss,
+      bubbles, ultraviolet) and standard Go modules (x/term,
+      x/sys). (#43, #45, #48)
+    - Upgraded various GitHub Actions to their latest major
+      versions, including actions/configure-pages,
+      actions/deploy-pages, actions/upload-pages-artifact, and
+      peter-evans/create-pull-request. (#42)
+  * Breaking Changes
+    - None.
+
+-------------------------------------------------------------------

Old:
----
  lazyworktree-1.45.0.obscpio

New:
----
  lazyworktree-1.45.1.obscpio

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ lazyworktree.spec ++++++
--- /var/tmp/diff_new_pack.RcRVfx/_old  2026-04-30 20:30:37.415370615 +0200
+++ /var/tmp/diff_new_pack.RcRVfx/_new  2026-04-30 20:30:37.435371429 +0200
@@ -17,7 +17,7 @@
 
 
 Name:           lazyworktree
-Version:        1.45.0
+Version:        1.45.1
 Release:        0
 Summary:        Easy Git worktree management for the terminal
 License:        Apache-2.0

++++++ _service ++++++
--- /var/tmp/diff_new_pack.RcRVfx/_old  2026-04-30 20:30:37.787385740 +0200
+++ /var/tmp/diff_new_pack.RcRVfx/_new  2026-04-30 20:30:37.831387530 +0200
@@ -3,7 +3,7 @@
     <param name="url">https://github.com/chmouel/lazyworktree.git</param>
     <param name="scm">git</param>
     <param name="exclude">.git</param>
-    <param name="revision">refs/tags/v1.45.0</param>
+    <param name="revision">refs/tags/v1.45.1</param>
     <param name="versionformat">@PARENT_TAG@</param>
     <param name="versionrewrite-pattern">v(.*)</param>
     <param name="changesgenerate">enable</param>

++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.RcRVfx/_old  2026-04-30 20:30:38.043396149 +0200
+++ /var/tmp/diff_new_pack.RcRVfx/_new  2026-04-30 20:30:38.075397451 +0200
@@ -1,6 +1,6 @@
 <servicedata>
 <service name="tar_scm">
                 <param 
name="url">https://github.com/chmouel/lazyworktree.git</param>
-              <param 
name="changesrevision">0d2cb7b5d6f1a57f66620ed34e02b3d9c996a5cd</param></service></servicedata>
+              <param 
name="changesrevision">931d43166ed25419350a6b2f7c4e2c5db0aeb91e</param></service></servicedata>
 (No newline at EOF)
 

++++++ lazyworktree-1.45.0.obscpio -> lazyworktree-1.45.1.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lazyworktree-1.45.0/AGENTS.md 
new/lazyworktree-1.45.1/AGENTS.md
--- old/lazyworktree-1.45.0/AGENTS.md   2026-03-28 18:26:04.000000000 +0100
+++ new/lazyworktree-1.45.1/AGENTS.md   2026-04-28 15:17:12.000000000 +0200
@@ -20,6 +20,9 @@
   - internal help text/template in `internal/app/screen/help.go`
   - generated CLI docs via `make docs-sync`
   - relevant website docs
+- If you change shell integration helpers or completion behaviour, update:
+  - `shell/README.md`
+  - the relevant helper in `shell/functions.bash`, `shell/functions.zsh`, or 
`shell/functions.fish`
 - Do not add to README.md unless important to ask the human to approve.
 - Don't ever do commit unless you are being explicitly asked for it.
 - If you get asked to commit then use this rules:
@@ -43,6 +46,17 @@
   - Remove any overly casual Americanisms
   - Keep technical precision whilst maintaining readability
 
+## Documentation review rules
+
+When reviewing docs:
+
+- Compare documentation to code, tests, schemas, and CLI help.
+- Prefer exact evidence over intuition.
+- Separate confirmed drift from likely drift.
+- Treat setup, migration, config, and API inaccuracies as high severity.
+- Suggest minimal, reviewable doc patches.
+- Preserve existing terminology unless the implementation clearly changed.
+
 ### Website documentation (`docs/`)
 
 The website is built with MkDocs Material and configured in `mkdocs.yml`.
@@ -73,6 +87,7 @@
 - `make docs-sync` — regenerates `docs/cli/commands.md` and 
`docs/cli/flags.md` from source code.
 - `make docs-check` — runs sync + strict MkDocs build to catch broken links or 
missing pages.
 - `make docs-build` / `make docs-serve` — build or preview the site locally.
+- `make coverage` — writes `coverage.out` with per-function coverage details 
for coverage-sensitive changes.
 - When adding a new page, also add it to `mkdocs.yml` under the appropriate 
`nav:` section.
 
 ## UI
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lazyworktree-1.45.0/Casks/lazyworktree.rb 
new/lazyworktree-1.45.1/Casks/lazyworktree.rb
--- old/lazyworktree-1.45.0/Casks/lazyworktree.rb       2026-03-28 
18:26:04.000000000 +0100
+++ new/lazyworktree-1.45.1/Casks/lazyworktree.rb       2026-04-28 
15:17:12.000000000 +0200
@@ -1,43 +1,44 @@
 # This file was generated by GoReleaser. DO NOT EDIT.
 cask "lazyworktree" do
-  name "lazyworktree"
-  desc "lazyworktree - A TUI tool to manage git worktrees"
-  homepage "https://github.com/chmouel/lazyworktree";
-  version "1.44.0"
-
-  livecheck do
-    skip "Auto-generated on release."
-  end
-
-  binary "lazyworktree"
-  depends_on formula: [
-      "git",
-      "git-delta",
-      "lazygit",
-      "less",
-      "tmux",
-    ]
+  version "1.45.0"
 
   on_macos do
     url 
"https://github.com/chmouel/lazyworktree/releases/download/v#{version}/lazyworktree_Darwin_all.tar.gz";
-    sha256 "709d4bf8d076c7116ab1d0cdf6077525050af778b5255c3eaa289b44953342d1"
+    sha256 "9ebd981b7f78003d67d4eeae36fb945bd6dcc0bab63c32931a32328418712ddf"
   end
 
   on_linux do
     on_intel do
       url 
"https://github.com/chmouel/lazyworktree/releases/download/v#{version}/lazyworktree_Linux_x86_64.tar.gz";
-      sha256 "ba393e6c208de75440c988c61326b20f6c659bc0b6bc1dbd9b74330ecdaed6a3"
+      sha256 "3c289aad422e582286fd23683cbe3a2246f126a2381b3d10332a4b4e5a2126be"
     end
     on_arm do
       url 
"https://github.com/chmouel/lazyworktree/releases/download/v#{version}/lazyworktree_Linux_arm64.tar.gz";
-      sha256 "d45da9c2244029cd5036b30245e6aca7f4bfe42a0d02dfa112eada7d77912cf4"
+      sha256 "af367503321eb845028b1a60feda0255ae65172b7fb0dd0029840dfb6b1979e5"
     end
   end
 
-  caveats do
-    "On first run, macOS may block the binary. To remove the quarantine 
attribute:"
-    "  xattr -d com.apple.quarantine #{staged_path}/lazyworktree"
+  name "lazyworktree"
+  desc "lazyworktree - A TUI tool to manage git worktrees"
+  homepage "https://github.com/chmouel/lazyworktree";
+
+  livecheck do
+    skip "Auto-generated on release."
   end
+  depends_on formula: [
+      "git",
+      "git-delta",
+      "lazygit",
+      "less",
+      "tmux",
+    ]
+
+  binary "lazyworktree"
 
   # No zap stanza required
+
+  caveats <<~EOS
+    On first run, macOS may block the binary. To remove the quarantine 
attribute:
+      xattr -d com.apple.quarantine #{staged_path}/lazyworktree
+  EOS
 end
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lazyworktree-1.45.0/config.example.yaml 
new/lazyworktree-1.45.1/config.example.yaml
--- old/lazyworktree-1.45.0/config.example.yaml 2026-03-28 18:26:04.000000000 
+0100
+++ new/lazyworktree-1.45.1/config.example.yaml 2026-04-28 15:17:12.000000000 
+0200
@@ -2,7 +2,13 @@
 # CORE SETTINGS
 # ============================================================================
 
-# Directory where your worktrees will be created
+# Directory where your worktrees will be created.
+# lazyworktree automatically sets $LWT_REPO_PATH to the git repository root,
+# so you can place worktrees inside the repository itself:
+#   worktree_dir: $LWT_REPO_PATH/.worktrees
+# In repo-local mode the <repoName> path segment is omitted, giving:
+#   <repoRoot>/.worktrees/<worktreeName>
+# Remember to add the directory to .gitignore (e.g. echo '.worktrees' >> 
.gitignore).
 worktree_dir: ~/.local/share/worktrees
 
 # How worktrees are sorted in the list
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lazyworktree-1.45.0/docs/configuration/overview.md 
new/lazyworktree-1.45.1/docs/configuration/overview.md
--- old/lazyworktree-1.45.0/docs/configuration/overview.md      2026-03-28 
18:26:04.000000000 +0100
+++ new/lazyworktree-1.45.1/docs/configuration/overview.md      2026-04-28 
15:17:12.000000000 +0200
@@ -32,7 +32,7 @@
 
 ```bash
 git config --global lw.theme nord
-git config --local lw.sort_mode switched
+git config --local lw.sort-mode switched
 ```
 
 List configured keys:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lazyworktree-1.45.0/docs/configuration/reference.md 
new/lazyworktree-1.45.1/docs/configuration/reference.md
--- old/lazyworktree-1.45.0/docs/configuration/reference.md     2026-03-28 
18:26:04.000000000 +0100
+++ new/lazyworktree-1.45.1/docs/configuration/reference.md     2026-04-28 
15:17:12.000000000 +0200
@@ -5,7 +5,7 @@
 <!-- BEGIN GENERATED:config-reference -->
 | Key | Type | Default | Description |
 | --- | --- | --- | --- |
-| `worktree_dir` | `string` | `none` | Root directory for managed worktrees. |
+| `worktree_dir` | `string` | `none` | Root directory for managed worktrees. 
Supports `$LWT_REPO_PATH` (auto-set to the git repository root) for repo-local 
placement, e.g. `$LWT_REPO_PATH/.worktrees`. When the directory is inside the 
repository, the `<repoName>` path segment is omitted automatically. |
 | `theme` | `string` | `auto-detect` | UI theme selection. |
 | `icon_set` | `enum(nerd-font-v3\|text)` | `nerd-font-v3` | Icon rendering 
mode for terminal compatibility. |
 | `layout` | `enum(default\|top)` | `default` | Pane layout strategy. |
@@ -16,6 +16,7 @@
 | `ci_auto_refresh` | `bool` | `false` | Enable periodic CI refresh for GitHub 
repositories. |
 | `auto_fetch_prs` | `bool` | `false` | Automatically fetch PR/MR data. |
 | `disable_pr` | `bool` | `false` | Disable PR/MR integration. |
+| `prune_stale_branches` | `bool` | `false` | Include merged branches without 
worktrees in prune. |
 | `search_auto_select` | `bool` | `false` | Focus filter and auto-select first 
match. |
 | `fuzzy_finder_input` | `bool` | `false` | Enable fuzzy helper input in 
selection dialogues. |
 | `max_name_length` | `int` | `95` | Maximum displayed worktree name length. |
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lazyworktree-1.45.0/docs/configuration.md 
new/lazyworktree-1.45.1/docs/configuration.md
--- old/lazyworktree-1.45.0/docs/configuration.md       2026-03-28 
18:26:04.000000000 +0100
+++ new/lazyworktree-1.45.1/docs/configuration.md       2026-04-28 
15:17:12.000000000 +0200
@@ -2,6 +2,20 @@
 
 Default worktree location: 
`~/.local/share/worktrees/<organization>-<repo_name>`.
 
+To keep worktrees inside the repository itself, set `worktree_dir` to use 
`$LWT_REPO_PATH` — lazyworktree automatically exports this variable to the git 
repository root before loading config:
+
+```bash
+git config --local lw.worktree-dir '$LWT_REPO_PATH/.worktrees'
+```
+
+Worktrees are then created at `<repoRoot>/.worktrees/<worktreeName>` (the 
`<repoName>` segment is omitted when the directory is already inside the 
repository).
+
+Add the directory to `.gitignore` so it does not appear in `git status`:
+
+```bash
+echo '.worktrees' >> .gitignore
+```
+
 <div class="lw-callout">
   <p><strong>Find settings quickly:</strong> use the map below, then jump to 
the relevant settings group.</p>
 </div>
@@ -101,12 +115,13 @@
 ```bash
 # Set globally
 git config --global lw.theme nord
-git config --global lw.worktree_dir ~/.local/share/worktrees
+git config --global lw.worktree-dir ~/.local/share/worktrees
 
 # Set per-repository
 git config --local lw.theme dracula
-git config --local lw.init_commands "link_topsymlinks"
-git config --local lw.init_commands "npm install"  # Multi-values supported
+git config --local lw.worktree-dir '$LWT_REPO_PATH/.worktrees'  # Repo-local 
worktrees
+git config --local lw.init-commands "link_topsymlinks"
+git config --local lw.init-commands "npm install"  # Multi-values supported
 ```
 
 To view configured values:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lazyworktree-1.45.0/docs/keybindings.md 
new/lazyworktree-1.45.1/docs/keybindings.md
--- old/lazyworktree-1.45.0/docs/keybindings.md 2026-03-28 18:26:04.000000000 
+0100
+++ new/lazyworktree-1.45.1/docs/keybindings.md 2026-04-28 15:17:12.000000000 
+0200
@@ -30,7 +30,7 @@
 | `D` | Delete selected worktree |
 | `d` | View diff in pager (worktree or commit, depending on pane) |
 | `A` | Absorb worktree into main |
-| `X` | Prune merged worktrees (refreshes PR data, checks merge status) |
+| `X` | Prune merged worktrees and stale branches (refreshes PR data, checks 
merge status; stale branches require `prune_stale_branches` config) |
 | `!` | Run arbitrary command in selected worktree (with command history) |
 | `v` | View CI checks (Enter opens browser, `Ctrl+v` opens logs in pager) |
 | `o` | Open PR/MR in browser (or root repo in editor if main branch with 
merged/closed/no PR) |
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lazyworktree-1.45.0/go.mod 
new/lazyworktree-1.45.1/go.mod
--- old/lazyworktree-1.45.0/go.mod      2026-03-28 18:26:04.000000000 +0100
+++ new/lazyworktree-1.45.1/go.mod      2026-04-28 15:17:12.000000000 +0200
@@ -1,27 +1,27 @@
 module github.com/chmouel/lazyworktree
 
-go 1.25
+go 1.25.0
 
 require (
-       charm.land/bubbles/v2 v2.0.0
-       charm.land/bubbletea/v2 v2.0.0
-       charm.land/lipgloss/v2 v2.0.0
+       charm.land/bubbles/v2 v2.1.0
+       charm.land/bubbletea/v2 v2.0.6
+       charm.land/lipgloss/v2 v2.0.3
        github.com/charmbracelet/lipgloss v1.1.0
-       github.com/charmbracelet/x/ansi v0.11.6
+       github.com/charmbracelet/x/ansi v0.11.7
        github.com/fsnotify/fsnotify v1.9.0
        github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
        github.com/muesli/reflow v0.3.0
        github.com/stretchr/testify v1.11.1
-       github.com/urfave/cli/v3 v3.6.2
-       golang.org/x/term v0.40.0
+       github.com/urfave/cli/v3 v3.8.0
+       golang.org/x/term v0.42.0
        gopkg.in/yaml.v3 v3.0.1
 )
 
 require (
        github.com/atotto/clipboard v0.1.4 // indirect
        github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
-       github.com/charmbracelet/colorprofile v0.4.2 // indirect
-       github.com/charmbracelet/ultraviolet v0.0.0-20260223171050-89c142e4aa73 
// indirect
+       github.com/charmbracelet/colorprofile v0.4.3 // indirect
+       github.com/charmbracelet/ultraviolet v0.0.0-20260416161146-9c68a866306c 
// indirect
        github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
        github.com/charmbracelet/x/term v0.2.2 // indirect
        github.com/charmbracelet/x/termios v0.1.1 // indirect
@@ -29,14 +29,14 @@
        github.com/clipperhouse/displaywidth v0.11.0 // indirect
        github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
        github.com/davecgh/go-spew v1.1.1 // indirect
-       github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
+       github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
        github.com/mattn/go-isatty v0.0.20 // indirect
-       github.com/mattn/go-runewidth v0.0.20 // indirect
+       github.com/mattn/go-runewidth v0.0.23 // indirect
        github.com/muesli/cancelreader v0.2.2 // indirect
        github.com/muesli/termenv v0.16.0 // indirect
        github.com/pmezard/go-difflib v1.0.0 // indirect
        github.com/rivo/uniseg v0.4.7 // indirect
        github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
-       golang.org/x/sync v0.19.0 // indirect
-       golang.org/x/sys v0.41.0 // indirect
+       golang.org/x/sync v0.20.0 // indirect
+       golang.org/x/sys v0.43.0 // indirect
 )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lazyworktree-1.45.0/go.sum 
new/lazyworktree-1.45.1/go.sum
--- old/lazyworktree-1.45.0/go.sum      2026-03-28 18:26:04.000000000 +0100
+++ new/lazyworktree-1.45.1/go.sum      2026-04-28 15:17:12.000000000 +0200
@@ -1,25 +1,25 @@
-charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s=
-charm.land/bubbles/v2 v2.0.0/go.mod 
h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI=
-charm.land/bubbletea/v2 v2.0.0 h1:p0d6CtWyJXJ9GfzMpUUqbP/XUUhhlk06+vCKWmox1wQ=
-charm.land/bubbletea/v2 v2.0.0/go.mod 
h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ=
-charm.land/lipgloss/v2 v2.0.0 h1:sd8N/B3x892oiOjFfBQdXBQp3cAkvjGaU5TvVZC3ivo=
-charm.land/lipgloss/v2 v2.0.0/go.mod 
h1:w6SnmsBFBmEFBodiEDurGS/sdUY/u1+v72DqUzc6J14=
+charm.land/bubbles/v2 v2.1.0 h1:YSnNh5cPYlYjPxRrzs5VEn3vwhtEn3jVGRBT3M7/I0g=
+charm.land/bubbles/v2 v2.1.0/go.mod 
h1:l97h4hym2hvWBVfmJDtrEHHCtkIKeTEb3TTJ4ZOB3wY=
+charm.land/bubbletea/v2 v2.0.6 h1:UHN/91OyuhaOFGSrBXQ/hMZD8IO1Uc4BvHlgHXL2WJo=
+charm.land/bubbletea/v2 v2.0.6/go.mod 
h1:MH/D8ZLlN3op37vQvijKuU29g3rqTp+aQapURFonF9g=
+charm.land/lipgloss/v2 v2.0.3 h1:yM2zJ4Cf5Y51b7RHIwioil4ApI/aypFXXVHSwlM6RzU=
+charm.land/lipgloss/v2 v2.0.3/go.mod 
h1:7myLU9iG/3xluAWzpY/fSxYYHCgoKTie7laxk6ATwXA=
 github.com/MakeNowJust/heredoc v1.0.0 
h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
 github.com/MakeNowJust/heredoc v1.0.0/go.mod 
h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
 github.com/atotto/clipboard v0.1.4 
h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
 github.com/atotto/clipboard v0.1.4/go.mod 
h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
 github.com/aymanbagabas/go-osc52/v2 v2.0.1 
h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
 github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod 
h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
-github.com/aymanbagabas/go-udiff v0.4.0 
h1:TKnLPh7IbnizJIBKFWa9mKayRUBQ9Kh1BPCk6w2PnYM=
-github.com/aymanbagabas/go-udiff v0.4.0/go.mod 
h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
-github.com/charmbracelet/colorprofile v0.4.2 
h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY=
-github.com/charmbracelet/colorprofile v0.4.2/go.mod 
h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8=
+github.com/aymanbagabas/go-udiff v0.4.1 
h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=
+github.com/aymanbagabas/go-udiff v0.4.1/go.mod 
h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
+github.com/charmbracelet/colorprofile v0.4.3 
h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
+github.com/charmbracelet/colorprofile v0.4.3/go.mod 
h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
 github.com/charmbracelet/lipgloss v1.1.0 
h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
 github.com/charmbracelet/lipgloss v1.1.0/go.mod 
h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
-github.com/charmbracelet/ultraviolet v0.0.0-20260223171050-89c142e4aa73 
h1:Af/L28Xh+pddhouT/6lJ7IAIYfu5tWJOB0iqt+mXsYM=
-github.com/charmbracelet/ultraviolet v0.0.0-20260223171050-89c142e4aa73/go.mod 
h1:E6/0abq9uG2SnM8IbLB9Y5SW09uIgfaFETk8aRzgXUQ=
-github.com/charmbracelet/x/ansi v0.11.6 
h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
-github.com/charmbracelet/x/ansi v0.11.6/go.mod 
h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
+github.com/charmbracelet/ultraviolet v0.0.0-20260416161146-9c68a866306c 
h1:a+Q3cOt8vEb6ETG/st32Qjm8R5fdI9wSKb3tqPISnoY=
+github.com/charmbracelet/ultraviolet v0.0.0-20260416161146-9c68a866306c/go.mod 
h1:bAAz7dh/FTYfC+oiHavL4mX1tOIBZ0ZwYjSi3qE6ivM=
+github.com/charmbracelet/x/ansi v0.11.7 
h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI=
+github.com/charmbracelet/x/ansi v0.11.7/go.mod 
h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ=
 github.com/charmbracelet/x/cellbuf v0.0.15 
h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
 github.com/charmbracelet/x/cellbuf v0.0.15/go.mod 
h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
 github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f 
h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=
@@ -40,13 +40,13 @@
 github.com/fsnotify/fsnotify v1.9.0/go.mod 
h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 
h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod 
h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
-github.com/lucasb-eyer/go-colorful v1.3.0 
h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
-github.com/lucasb-eyer/go-colorful v1.3.0/go.mod 
h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
+github.com/lucasb-eyer/go-colorful v1.4.0 
h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
+github.com/lucasb-eyer/go-colorful v1.4.0/go.mod 
h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
 github.com/mattn/go-isatty v0.0.20 
h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 github.com/mattn/go-isatty v0.0.20/go.mod 
h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
 github.com/mattn/go-runewidth v0.0.12/go.mod 
h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
-github.com/mattn/go-runewidth v0.0.20 
h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ=
-github.com/mattn/go-runewidth v0.0.20/go.mod 
h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
+github.com/mattn/go-runewidth v0.0.23 
h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
+github.com/mattn/go-runewidth v0.0.23/go.mod 
h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
 github.com/muesli/cancelreader v0.2.2 
h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
 github.com/muesli/cancelreader v0.2.2/go.mod 
h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
 github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
@@ -61,19 +61,19 @@
 github.com/rivo/uniseg v0.4.7/go.mod 
h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
 github.com/stretchr/testify v1.11.1 
h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
 github.com/stretchr/testify v1.11.1/go.mod 
h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
-github.com/urfave/cli/v3 v3.6.2 h1:lQuqiPrZ1cIz8hz+HcrG0TNZFxU70dPZ3Yl+pSrH9A8=
-github.com/urfave/cli/v3 v3.6.2/go.mod 
h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
+github.com/urfave/cli/v3 v3.8.0 h1:XqKPrm0q4P0q5JpoclYoCAv0/MIvH/jZ2umzuf8pNTI=
+github.com/urfave/cli/v3 v3.8.0/go.mod 
h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e 
h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod 
h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
 golang.org/x/exp v0.0.0-20231006140011-7918f672742d 
h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
 golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod 
h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
-golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
-golang.org/x/sync v0.19.0/go.mod 
h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
+golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
+golang.org/x/sync v0.20.0/go.mod 
h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
-golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
-golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
-golang.org/x/term v0.40.0/go.mod 
h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
+golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
+golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
+golang.org/x/term v0.42.0/go.mod 
h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 
h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod 
h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lazyworktree-1.45.0/hack/docsync/main.go 
new/lazyworktree-1.45.1/hack/docsync/main.go
--- old/lazyworktree-1.45.0/hack/docsync/main.go        2026-03-28 
18:26:04.000000000 +0100
+++ new/lazyworktree-1.45.1/hack/docsync/main.go        2026-04-28 
15:17:12.000000000 +0200
@@ -330,6 +330,7 @@
                "sort_by_active":               "bool (legacy)",
                "auto_fetch_prs":               "bool",
                "disable_pr":                   "bool",
+               "prune_stale_branches":         "bool",
                "auto_refresh":                 "bool",
                "ci_auto_refresh":              "bool",
                "refresh_interval":             "int",
@@ -364,7 +365,7 @@
        }
 
        descByKey := map[string]string{
-               "worktree_dir":                 "Root directory for managed 
worktrees.",
+               "worktree_dir":                 "Root directory for managed 
worktrees. Supports `$LWT_REPO_PATH` (auto-set to the git repository root) for 
repo-local placement, e.g. `$LWT_REPO_PATH/.worktrees`. When the directory is 
inside the repository, the `<repoName>` path segment is omitted automatically.",
                "debug_log":                    "Debug log file path.",
                "pager":                        "Pager for command output 
views.",
                "ci_script_pager":              "Dedicated pager for CI logs.",
@@ -376,6 +377,7 @@
                "sort_by_active":               "Compatibility key for older 
sort configuration.",
                "auto_fetch_prs":               "Automatically fetch PR/MR 
data.",
                "disable_pr":                   "Disable PR/MR integration.",
+               "prune_stale_branches":         "Include merged branches 
without worktrees in prune.",
                "auto_refresh":                 "Enable background refresh of 
repository state.",
                "ci_auto_refresh":              "Enable periodic CI refresh for 
GitHub repositories.",
                "refresh_interval":             "Background refresh cadence in 
seconds.",
@@ -413,6 +415,7 @@
                "sort_mode":                  defaults["SortMode"],
                "auto_fetch_prs":             defaults["AutoFetchPRs"],
                "disable_pr":                 "false",
+               "prune_stale_branches":       "false",
                "auto_refresh":               defaults["AutoRefresh"],
                "ci_auto_refresh":            "false",
                "refresh_interval":           
defaults["RefreshIntervalSeconds"],
@@ -783,6 +786,7 @@
                "ci_auto_refresh",
                "auto_fetch_prs",
                "disable_pr",
+               "prune_stale_branches",
                "search_auto_select",
                "fuzzy_finder_input",
                "max_name_length",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lazyworktree-1.45.0/internal/app/app.go 
new/lazyworktree-1.45.1/internal/app/app.go
--- old/lazyworktree-1.45.0/internal/app/app.go 2026-03-28 18:26:04.000000000 
+0100
+++ new/lazyworktree-1.45.1/internal/app/app.go 2026-04-28 15:17:12.000000000 
+0200
@@ -112,11 +112,12 @@
                fetchedAt    time.Time
        }
        pruneResultMsg struct {
-               worktrees      []*models.WorktreeInfo
-               err            error
-               pruned         int
-               failed         int
-               orphansDeleted int
+               worktrees       []*models.WorktreeInfo
+               err             error
+               pruned          int
+               failed          int
+               orphansDeleted  int
+               branchesDeleted int
        }
        absorbMergeResultMsg struct {
                path   string
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lazyworktree-1.45.0/internal/app/app_helpers.go 
new/lazyworktree-1.45.1/internal/app/app_helpers.go
--- old/lazyworktree-1.45.0/internal/app/app_helpers.go 2026-03-28 
18:26:04.000000000 +0100
+++ new/lazyworktree-1.45.1/internal/app/app_helpers.go 2026-04-28 
15:17:12.000000000 +0200
@@ -13,6 +13,7 @@
        "charm.land/lipgloss/v2"
        appscreen "github.com/chmouel/lazyworktree/internal/app/screen"
        "github.com/chmouel/lazyworktree/internal/app/services"
+       "github.com/chmouel/lazyworktree/internal/cli"
        log "github.com/chmouel/lazyworktree/internal/log"
        "github.com/chmouel/lazyworktree/internal/models"
        "github.com/chmouel/lazyworktree/internal/utils"
@@ -396,7 +397,14 @@
 }
 
 func (m *Model) getRepoWorktreeDir() string {
-       return filepath.Join(m.getWorktreeDir(), m.getRepoKey())
+       dir := m.getWorktreeDir()
+       if m.state.services.git != nil {
+               mainPath := m.state.services.git.GetMainWorktreePath(m.ctx)
+               if cli.IsRepoLocal(dir, mainPath) {
+                       return dir
+               }
+       }
+       return filepath.Join(dir, m.getRepoKey())
 }
 
 // normalizePath returns a canonical path for comparison.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/lazyworktree-1.45.0/internal/app/handlers_messages_test.go 
new/lazyworktree-1.45.1/internal/app/handlers_messages_test.go
--- old/lazyworktree-1.45.0/internal/app/handlers_messages_test.go      
2026-03-28 18:26:04.000000000 +0100
+++ new/lazyworktree-1.45.1/internal/app/handlers_messages_test.go      
2026-04-28 15:17:12.000000000 +0200
@@ -417,6 +417,38 @@
        }
 }
 
+func TestHandlePruneResultStaleBranches(t *testing.T) {
+       cfg := &config.AppConfig{WorktreeDir: t.TempDir()}
+       m := NewModel(cfg, "")
+
+       msg := pruneResultMsg{worktrees: []*models.WorktreeInfo{}, pruned: 1, 
branchesDeleted: 3, failed: 0, err: nil}
+       updated, _ := m.handlePruneResult(msg)
+       updatedModel := updated.(*Model)
+
+       if !strings.Contains(updatedModel.statusContent, "Pruned 1") {
+               t.Errorf("expected status to include pruned count, got %q", 
updatedModel.statusContent)
+       }
+       if !strings.Contains(updatedModel.statusContent, "deleted 3 stale 
branches") {
+               t.Errorf("expected status to include stale branches count, got 
%q", updatedModel.statusContent)
+       }
+}
+
+func TestHandlePruneResultOnlyStaleBranches(t *testing.T) {
+       cfg := &config.AppConfig{WorktreeDir: t.TempDir()}
+       m := NewModel(cfg, "")
+
+       msg := pruneResultMsg{worktrees: []*models.WorktreeInfo{}, pruned: 0, 
branchesDeleted: 2, failed: 0, err: nil}
+       updated, _ := m.handlePruneResult(msg)
+       updatedModel := updated.(*Model)
+
+       if strings.Contains(updatedModel.statusContent, "Pruned 0") {
+               t.Errorf("should not show 'Pruned 0' when no worktrees pruned, 
got %q", updatedModel.statusContent)
+       }
+       if !strings.Contains(updatedModel.statusContent, "deleted 2 stale 
branches") {
+               t.Errorf("expected status to include stale branches count, got 
%q", updatedModel.statusContent)
+       }
+}
+
 func TestHandleAbsorbResultError(t *testing.T) {
        cfg := &config.AppConfig{WorktreeDir: t.TempDir()}
        m := NewModel(cfg, "")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lazyworktree-1.45.0/internal/app/messages.go 
new/lazyworktree-1.45.1/internal/app/messages.go
--- old/lazyworktree-1.45.0/internal/app/messages.go    2026-03-28 
18:26:04.000000000 +0100
+++ new/lazyworktree-1.45.1/internal/app/messages.go    2026-04-28 
15:17:12.000000000 +0200
@@ -211,6 +211,9 @@
        if msg.orphansDeleted > 0 {
                parts = append(parts, fmt.Sprintf("deleted %d orphaned 
directories", msg.orphansDeleted))
        }
+       if msg.branchesDeleted > 0 {
+               parts = append(parts, fmt.Sprintf("deleted %d stale branches", 
msg.branchesDeleted))
+       }
        if msg.failed > 0 {
                parts = append(parts, fmt.Sprintf("%d failed", msg.failed))
        }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lazyworktree-1.45.0/internal/app/screen/help.go 
new/lazyworktree-1.45.1/internal/app/screen/help.go
--- old/lazyworktree-1.45.0/internal/app/screen/help.go 2026-03-28 
18:26:04.000000000 +0100
+++ new/lazyworktree-1.45.1/internal/app/screen/help.go 2026-04-28 
15:17:12.000000000 +0200
@@ -155,7 +155,7 @@
 - m: Rename selected worktree
 - D: Delete selected worktree
 - A: Absorb worktree into main (merge or rebase based on configuration, then 
delete)
-- X: Prune merged worktrees (auto-refreshes PR data, then checks PR/branch 
merge status)
+- X: Prune merged worktrees and stale branches (auto-refreshes PR data; enable 
prune_stale_branches to include merged branches without worktrees)
 - !: Run arbitrary command in selected worktree
 
 **{{HELP_BRANCH_NAMING}}Branch Naming**
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/lazyworktree-1.45.0/internal/app/services/agent_processes.go 
new/lazyworktree-1.45.1/internal/app/services/agent_processes.go
--- old/lazyworktree-1.45.0/internal/app/services/agent_processes.go    
2026-03-28 18:26:04.000000000 +0100
+++ new/lazyworktree-1.45.1/internal/app/services/agent_processes.go    
2026-04-28 15:17:12.000000000 +0200
@@ -229,6 +229,9 @@
                        continue
                }
                if shellCommandExpected {
+                       if strings.EqualFold(token, "exec") {
+                               continue
+                       }
                        return isClaudeCLIToken(token)
                }
                switch token {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/lazyworktree-1.45.0/internal/app/services/agent_processes_test.go 
new/lazyworktree-1.45.1/internal/app/services/agent_processes_test.go
--- old/lazyworktree-1.45.0/internal/app/services/agent_processes_test.go       
2026-03-28 18:26:04.000000000 +0100
+++ new/lazyworktree-1.45.1/internal/app/services/agent_processes_test.go       
2026-04-28 15:17:12.000000000 +0200
@@ -145,6 +145,18 @@
        }
 }
 
+func TestClassifyAgentProcessMatchesShellExecWrapper(t *testing.T) {
+       t.Parallel()
+
+       agent, source, ok := classifyAgentProcess("bash", "bash -lc 'exec 
claude --print'")
+       if !ok {
+               t.Fatal("expected shell exec wrapper to be detected")
+       }
+       if agent != models.AgentKindClaude || source != "cli" {
+               t.Fatalf("expected Claude CLI match, got %q %q", agent, source)
+       }
+}
+
 func stringsJoin(lines ...string) string {
        return strings.Join(lines, "\n")
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/lazyworktree-1.45.0/internal/app/services/worktree.go 
new/lazyworktree-1.45.1/internal/app/services/worktree.go
--- old/lazyworktree-1.45.0/internal/app/services/worktree.go   2026-03-28 
18:26:04.000000000 +0100
+++ new/lazyworktree-1.45.1/internal/app/services/worktree.go   2026-04-28 
15:17:12.000000000 +0200
@@ -36,8 +36,8 @@
        // Absorb merges or rebases a worktree into the main branch.
        Absorb(ctx context.Context, wt *models.WorktreeInfo, mainWorktree 
*models.WorktreeInfo, mergeMethod string) error
 
-       // GetPruneCandidates identifies worktrees that have been merged and 
are candidates for pruning.
-       GetPruneCandidates(ctx context.Context, worktrees 
[]*models.WorktreeInfo) ([]PruneCandidate, error)
+       // GetPruneCandidates identifies worktrees and stale branches that have 
been merged and are candidates for pruning.
+       GetPruneCandidates(ctx context.Context, worktrees 
[]*models.WorktreeInfo, includeStaleBranches bool) ([]PruneCandidate, error)
 
        // ExecuteCommands runs a list of shell commands in the specified 
directory.
        ExecuteCommands(ctx context.Context, commands []string, cwd string, env 
map[string]string) error
@@ -60,10 +60,11 @@
        Env           map[string]string
 }
 
-// PruneCandidate represents a worktree that is a candidate for pruning.
+// PruneCandidate represents a worktree or stale branch that is a candidate 
for pruning.
 type PruneCandidate struct {
-       Worktree *models.WorktreeInfo
-       Source   string // "pr", "git", or "both"
+       Worktree *models.WorktreeInfo // nil for branch-only candidates
+       Branch   string               // branch name (always set)
+       Source   string               // "pr", "git", or "both"
 }
 
 // GitService defines the subset of git operations needed by WorktreeService.
@@ -232,11 +233,15 @@
        return nil
 }
 
-func (s *worktreeService) GetPruneCandidates(ctx context.Context, worktrees 
[]*models.WorktreeInfo) ([]PruneCandidate, error) {
+func (s *worktreeService) GetPruneCandidates(ctx context.Context, worktrees 
[]*models.WorktreeInfo, includeStaleBranches bool) ([]PruneCandidate, error) {
        mainBranch := s.git.GetMainBranch(ctx)
 
        wtBranches := make(map[string]*models.WorktreeInfo)
+       checkedOutBranches := make(map[string]struct{})
        for _, wt := range worktrees {
+               if wt.Branch != "" {
+                       checkedOutBranches[wt.Branch] = struct{}{}
+               }
                if !wt.IsMain {
                        wtBranches[wt.Branch] = wt
                }
@@ -250,7 +255,7 @@
                        continue
                }
                if wt.PR != nil && strings.EqualFold(wt.PR.State, "MERGED") {
-                       candidateMap[wt.Branch] = PruneCandidate{Worktree: wt, 
Source: "pr"}
+                       candidateMap[wt.Branch] = PruneCandidate{Worktree: wt, 
Branch: wt.Branch, Source: "pr"}
                }
        }
 
@@ -262,7 +267,21 @@
                                existing.Source = "both"
                                candidateMap[branch] = existing
                        } else {
-                               candidateMap[branch] = PruneCandidate{Worktree: 
wt, Source: "git"}
+                               candidateMap[branch] = PruneCandidate{Worktree: 
wt, Branch: branch, Source: "git"}
+                       }
+               }
+       }
+
+       // 3. Branch-only detection: merged branches with no worktree
+       if includeStaleBranches {
+               for _, branch := range mergedBranches {
+                       if _, checkedOut := checkedOutBranches[branch]; 
checkedOut {
+                               continue
+                       }
+                       if _, exists := wtBranches[branch]; !exists {
+                               if _, found := candidateMap[branch]; !found {
+                                       candidateMap[branch] = 
PruneCandidate{Worktree: nil, Branch: branch, Source: "git"}
+                               }
                        }
                }
        }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/lazyworktree-1.45.0/internal/app/services/worktree_test.go 
new/lazyworktree-1.45.1/internal/app/services/worktree_test.go
--- old/lazyworktree-1.45.0/internal/app/services/worktree_test.go      
1970-01-01 01:00:00.000000000 +0100
+++ new/lazyworktree-1.45.1/internal/app/services/worktree_test.go      
2026-04-28 15:17:12.000000000 +0200
@@ -0,0 +1,122 @@
+package services
+
+import (
+       "context"
+       "testing"
+
+       "github.com/chmouel/lazyworktree/internal/models"
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/require"
+)
+
+type mockGitService struct {
+       mainBranch     string
+       mergedBranches []string
+}
+
+func (m *mockGitService) RunGit(_ context.Context, _ []string, _ string, _ 
[]int, _, _ bool) string {
+       return ""
+}
+
+func (m *mockGitService) RunCommandChecked(_ context.Context, _ []string, _, _ 
string) bool {
+       return true
+}
+
+func (m *mockGitService) GetMainBranch(_ context.Context) string {
+       return m.mainBranch
+}
+
+func (m *mockGitService) GetMergedBranches(_ context.Context, _ string) 
[]string {
+       return m.mergedBranches
+}
+
+func (m *mockGitService) RenameWorktree(_ context.Context, _, _, _, _ string) 
bool {
+       return true
+}
+
+func (m *mockGitService) ExecuteCommands(_ context.Context, _ []string, _ 
string, _ map[string]string) error {
+       return nil
+}
+
+func (m *mockGitService) RunGitWithCombinedOutput(_ context.Context, _ 
[]string, _ string, _ map[string]string) ([]byte, error) {
+       return nil, nil
+}
+
+func TestGetPruneCandidatesWithStaleBranches(t *testing.T) {
+       git := &mockGitService{
+               mainBranch:     "main",
+               mergedBranches: []string{"feature-a", "feature-b", 
"stale-branch"},
+       }
+       svc := NewWorktreeService(git)
+
+       worktrees := []*models.WorktreeInfo{
+               {Path: "/repo/main", Branch: "main", IsMain: true},
+               {Path: "/repo/feature-a", Branch: "feature-a"},
+       }
+
+       // Without stale branches
+       candidates, err := svc.GetPruneCandidates(context.Background(), 
worktrees, false)
+       require.NoError(t, err)
+
+       // Only feature-a should be a candidate (has worktree and is merged)
+       assert.Len(t, candidates, 1)
+       assert.Equal(t, "feature-a", candidates[0].Branch)
+       assert.NotNil(t, candidates[0].Worktree)
+
+       // With stale branches
+       candidates, err = svc.GetPruneCandidates(context.Background(), 
worktrees, true)
+       require.NoError(t, err)
+
+       // feature-a (with worktree) + feature-b and stale-branch (branch-only)
+       assert.Len(t, candidates, 3)
+
+       branchMap := make(map[string]PruneCandidate)
+       for _, c := range candidates {
+               branchMap[c.Branch] = c
+       }
+
+       assert.NotNil(t, branchMap["feature-a"].Worktree)
+       assert.Nil(t, branchMap["feature-b"].Worktree)
+       assert.Equal(t, "git", branchMap["feature-b"].Source)
+       assert.Nil(t, branchMap["stale-branch"].Worktree)
+       assert.Equal(t, "git", branchMap["stale-branch"].Source)
+}
+
+func TestGetPruneCandidatesPRMerged(t *testing.T) {
+       git := &mockGitService{
+               mainBranch:     "main",
+               mergedBranches: []string{"feature-a"},
+       }
+       svc := NewWorktreeService(git)
+
+       worktrees := []*models.WorktreeInfo{
+               {Path: "/repo/main", Branch: "main", IsMain: true},
+               {Path: "/repo/feature-a", Branch: "feature-a", PR: 
&models.PRInfo{State: "MERGED"}},
+       }
+
+       candidates, err := svc.GetPruneCandidates(context.Background(), 
worktrees, false)
+       require.NoError(t, err)
+
+       assert.Len(t, candidates, 1)
+       assert.Equal(t, "both", candidates[0].Source)
+       assert.Equal(t, "feature-a", candidates[0].Branch)
+}
+
+func TestGetPruneCandidatesExcludesCheckedOutMainWorktreeBranch(t *testing.T) {
+       git := &mockGitService{
+               mainBranch:     "main",
+               mergedBranches: []string{"feature-root", "stale-branch"},
+       }
+       svc := NewWorktreeService(git)
+
+       worktrees := []*models.WorktreeInfo{
+               {Path: "/repo", Branch: "feature-root", IsMain: true},
+       }
+
+       candidates, err := svc.GetPruneCandidates(context.Background(), 
worktrees, true)
+       require.NoError(t, err)
+
+       assert.Len(t, candidates, 1)
+       assert.Equal(t, "stale-branch", candidates[0].Branch)
+       assert.Nil(t, candidates[0].Worktree)
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/lazyworktree-1.45.0/internal/app/worktree_operations.go 
new/lazyworktree-1.45.1/internal/app/worktree_operations.go
--- old/lazyworktree-1.45.0/internal/app/worktree_operations.go 2026-03-28 
18:26:04.000000000 +0100
+++ new/lazyworktree-1.45.1/internal/app/worktree_operations.go 2026-04-28 
15:17:12.000000000 +0200
@@ -714,7 +714,11 @@
        mainBranch := m.state.services.git.GetMainBranch(m.ctx)
 
        wtBranches := make(map[string]*models.WorktreeInfo)
+       checkedOutBranches := make(map[string]struct{})
        for _, wt := range m.state.data.worktrees {
+               if wt.Branch != "" {
+                       checkedOutBranches[wt.Branch] = struct{}{}
+               }
                if !wt.IsMain {
                        wtBranches[wt.Branch] = wt
                }
@@ -750,7 +754,21 @@
                }
        }
 
-       // 3. Detect orphaned directories (exist on disk but not in git 
worktree list)
+       // 3. Branch-only detection: merged branches with no worktree 
(config-gated)
+       if m.config.PruneStaleBranches {
+               for _, branch := range mergedBranches {
+                       if _, checkedOut := checkedOutBranches[branch]; 
checkedOut {
+                               continue
+                       }
+                       if _, exists := wtBranches[branch]; !exists {
+                               if _, found := candidateMap[branch]; !found {
+                                       candidateMap[branch] = candidate{wt: 
nil, source: "git"}
+                               }
+                       }
+               }
+       }
+
+       // 4. Detect orphaned directories (exist on disk but not in git 
worktree list)
        orphanedDirs := m.findOrphanedWorktreeDirs()
 
        if len(candidateMap) == 0 && len(orphanedDirs) == 0 {
@@ -761,8 +779,19 @@
        // Build checklist items (pre-check clean worktrees, uncheck dirty ones)
        items := make([]appscreen.ChecklistItem, 0, 
len(candidateMap)+len(orphanedDirs))
 
-       // Add merged worktrees
+       // Add merged worktrees and stale branches
        for branch, info := range candidateMap {
+               if info.wt == nil {
+                       // Branch-only candidate (merged, no worktree)
+                       items = append(items, appscreen.ChecklistItem{
+                               ID:          "branch:" + branch,
+                               Label:       branch,
+                               Description: fmt.Sprintf("Branch: %s (merged, 
no worktree)", branch),
+                               Checked:     false, // Require explicit 
selection
+                       })
+                       continue
+               }
+
                // Get worktree name from path
                wtName := filepath.Base(info.wt.Path)
 
@@ -823,12 +852,15 @@
                        return nil
                }
 
-               // Separate worktrees from orphaned directories
+               // Separate worktrees, stale branches, and orphaned directories
                toPrune := make([]*models.WorktreeInfo, 0, len(selected))
                orphansToDelete := make([]string, 0)
+               branchesToDelete := make([]string, 0)
                for _, item := range selected {
                        if orphanPath, isOrphan := strings.CutPrefix(item.ID, 
"orphan:"); isOrphan {
                                orphansToDelete = append(orphansToDelete, 
orphanPath)
+                       } else if branchName, isBranch := 
strings.CutPrefix(item.ID, "branch:"); isBranch {
+                               branchesToDelete = append(branchesToDelete, 
branchName)
                        } else if wt, exists := wtBranches[item.ID]; exists {
                                toPrune = append(toPrune, wt)
                        }
@@ -863,6 +895,16 @@
                                }
                        }
 
+                       // Delete stale branches (merged, no worktree)
+                       branchesDeleted := 0
+                       for _, branch := range branchesToDelete {
+                               if 
m.state.services.git.RunCommandChecked(m.ctx, []string{"git", "branch", "-D", 
branch}, "", fmt.Sprintf("Failed to delete branch %s", branch)) {
+                                       branchesDeleted++
+                               } else {
+                                       failed++
+                               }
+                       }
+
                        // Delete orphaned directories
                        // Re-fetch valid paths to ensure we have current state
                        validPaths := m.getValidWorktreePaths()
@@ -893,11 +935,12 @@
 
                        worktrees, err := 
m.state.services.git.GetWorktrees(m.ctx)
                        return pruneResultMsg{
-                               worktrees:      worktrees,
-                               err:            err,
-                               pruned:         pruned,
-                               failed:         failed,
-                               orphansDeleted: orphansDeleted,
+                               worktrees:       worktrees,
+                               err:             err,
+                               pruned:          pruned,
+                               failed:          failed,
+                               orphansDeleted:  orphansDeleted,
+                               branchesDeleted: branchesDeleted,
                        }
                }
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/lazyworktree-1.45.0/internal/app/worktree_operations_test.go 
new/lazyworktree-1.45.1/internal/app/worktree_operations_test.go
--- old/lazyworktree-1.45.0/internal/app/worktree_operations_test.go    
2026-03-28 18:26:04.000000000 +0100
+++ new/lazyworktree-1.45.1/internal/app/worktree_operations_test.go    
2026-04-28 15:17:12.000000000 +0200
@@ -647,6 +647,54 @@
        }
 }
 
+func TestPerformMergedWorktreeCheckExcludesCheckedOutMainWorktreeBranch(t 
*testing.T) {
+       repo := t.TempDir()
+       runGit(t, repo, "init", "-b", "main")
+       runGit(t, repo, "config", "user.email", "[email protected]")
+       runGit(t, repo, "config", "user.name", "Test User")
+       runGit(t, repo, "config", "commit.gpgsign", "false")
+
+       filePath := filepath.Join(repo, "file.txt")
+       if err := os.WriteFile(filePath, []byte("base\n"), 0o600); err != nil {
+               t.Fatalf("write initial file: %v", err)
+       }
+       runGit(t, repo, "add", "file.txt")
+       runGit(t, repo, "commit", "-m", "Initial commit")
+
+       runGit(t, repo, "checkout", "-b", "feature-root")
+       if err := os.WriteFile(filePath, []byte("feature\n"), 0o600); err != 
nil {
+               t.Fatalf("write feature file: %v", err)
+       }
+       runGit(t, repo, "commit", "-am", "Feature commit")
+       runGit(t, repo, "checkout", "main")
+       runGit(t, repo, "merge", "--no-ff", "--no-edit", "feature-root")
+       runGit(t, repo, "checkout", "feature-root")
+
+       withCwd(t, repo)
+
+       cfg := &config.AppConfig{
+               WorktreeDir:        t.TempDir(),
+               PruneStaleBranches: true,
+       }
+       m := NewModel(cfg, "")
+       m.state.data.worktrees = []*models.WorktreeInfo{
+               {Path: repo, Branch: "feature-root", IsMain: true},
+       }
+
+       cmd := m.performMergedWorktreeCheck()
+       if cmd != nil {
+               t.Fatal("expected no checklist command when only the 
checked-out main worktree branch is merged")
+       }
+       if !m.state.ui.screenManager.IsActive() || 
m.state.ui.screenManager.Type() != appscreen.TypeInfo {
+               t.Fatalf("expected info screen, got active=%v type=%v", 
m.state.ui.screenManager.IsActive(), m.state.ui.screenManager.Type())
+       }
+
+       infoScr := m.state.ui.screenManager.Current().(*appscreen.InfoScreen)
+       if infoScr.Message != "No merged worktrees or orphaned directories to 
prune." {
+               t.Fatalf("unexpected info modal: %q", infoScr.Message)
+       }
+}
+
 func TestShowPruneMergedUnknownHost(t *testing.T) {
        // Test that showPruneMerged skips PR fetch for unknown hosts
        cfg := &config.AppConfig{
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lazyworktree-1.45.0/internal/bootstrap/bootstrap.go 
new/lazyworktree-1.45.1/internal/bootstrap/bootstrap.go
--- old/lazyworktree-1.45.0/internal/bootstrap/bootstrap.go     2026-03-28 
18:26:04.000000000 +0100
+++ new/lazyworktree-1.45.1/internal/bootstrap/bootstrap.go     2026-04-28 
15:17:12.000000000 +0200
@@ -5,10 +5,12 @@
        "context"
        "fmt"
        "os"
+       "os/exec"
        "path/filepath"
        "slices"
        "sort"
        "strings"
+       "sync"
 
        tea "charm.land/bubbletea/v2"
        "github.com/chmouel/lazyworktree/internal/app"
@@ -20,6 +22,29 @@
        "github.com/urfave/cli/v3"
 )
 
+// gitToplevel returns the root of the current git repository, or "" if not in 
one.
+func gitToplevel() string {
+       out, err := exec.Command("git", "rev-parse", "--show-toplevel").Output()
+       if err != nil {
+               return ""
+       }
+       return strings.TrimSpace(string(out))
+}
+
+var initRepoPathOnce sync.Once
+
+// ensureRepoPath sets $LWT_REPO_PATH lazily so the git probe only runs when
+// config is actually loaded (skipped for --version, shell completion, etc.).
+func ensureRepoPath() {
+       initRepoPathOnce.Do(func() {
+               if root := gitToplevel(); root != "" {
+                       _ = os.Setenv("LWT_REPO_PATH", root)
+               } else {
+                       _ = os.Unsetenv("LWT_REPO_PATH")
+               }
+       })
+}
+
 // Run constructs the CLI application and executes it.
 // It returns an exit code suitable for os.Exit.
 func Run(args []string) int {
@@ -90,6 +115,8 @@
 }
 
 func runTUI(_ context.Context, cmd *cli.Command) error {
+       ensureRepoPath()
+
        if debugLog := cmd.String("debug-log"); debugLog != "" {
                expanded, err := utils.ExpandPath(debugLog)
                if err == nil {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lazyworktree-1.45.0/internal/bootstrap/commands.go 
new/lazyworktree-1.45.1/internal/bootstrap/commands.go
--- old/lazyworktree-1.45.0/internal/bootstrap/commands.go      2026-03-28 
18:26:04.000000000 +0100
+++ new/lazyworktree-1.45.1/internal/bootstrap/commands.go      2026-04-28 
15:17:12.000000000 +0200
@@ -1174,7 +1174,7 @@
                                        nonMain = append(nonMain, wt)
                                }
                        }
-                       if target, fErr := 
cli.FindWorktreeByPathOrName(worktreePath, nonMain, cfg.WorktreeDir, repoKey); 
fErr == nil {
+                       if target, fErr := 
cli.FindWorktreeByPathOrName(worktreePath, nonMain, cfg.WorktreeDir, repoKey, 
gitSvc.GetMainWorktreePath(ctx)); fErr == nil {
                                preDeleteName = filepath.Base(target.Path)
                                preDeletePath = target.Path
                                willDeleteBranch = deleteBranch && 
preDeleteName == target.Branch
@@ -1260,7 +1260,7 @@
                                        nonMain = append(nonMain, wt)
                                }
                        }
-                       if target, fErr := 
cli.FindWorktreeByPathOrName(worktreePath, nonMain, cfg.WorktreeDir, repoKey); 
fErr == nil {
+                       if target, fErr := 
cli.FindWorktreeByPathOrName(worktreePath, nonMain, cfg.WorktreeDir, repoKey, 
gitSvc.GetMainWorktreePath(ctx)); fErr == nil {
                                oldName = filepath.Base(target.Path)
                                oldPath = target.Path
                                sanitizedNewName = 
utils.SanitizeBranchName(newName, 100)
@@ -1521,7 +1521,7 @@
        var targetWorktree *models.WorktreeInfo
        if workspace != "" {
                // User provided workspace flag
-               targetWorktree, err = cli.FindWorktreeByPathOrName(workspace, 
worktrees, cfg.WorktreeDir, gitSvc.ResolveRepoName(ctx))
+               targetWorktree, err = cli.FindWorktreeByPathOrName(workspace, 
worktrees, cfg.WorktreeDir, gitSvc.ResolveRepoName(ctx), 
gitSvc.GetMainWorktreePath(ctx))
                if err != nil {
                        fmt.Fprintf(os.Stderr, "Error: %v\n", err)
                        return err
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/lazyworktree-1.45.0/internal/bootstrap/subcommands.go 
new/lazyworktree-1.45.1/internal/bootstrap/subcommands.go
--- old/lazyworktree-1.45.0/internal/bootstrap/subcommands.go   2026-03-28 
18:26:04.000000000 +0100
+++ new/lazyworktree-1.45.1/internal/bootstrap/subcommands.go   2026-04-28 
15:17:12.000000000 +0200
@@ -10,6 +10,8 @@
 
 // loadCLIConfig loads and configures the application configuration for CLI 
mode.
 func loadCLIConfig(configFileFlag, worktreeDirFlag string, configOverrides 
[]string) (*config.AppConfig, error) {
+       ensureRepoPath()
+
        cfg, err := config.LoadConfig(configFileFlag)
        if err != nil {
                fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lazyworktree-1.45.0/internal/cli/operations.go 
new/lazyworktree-1.45.1/internal/cli/operations.go
--- old/lazyworktree-1.45.0/internal/cli/operations.go  2026-03-28 
18:26:04.000000000 +0100
+++ new/lazyworktree-1.45.1/internal/cli/operations.go  2026-04-28 
15:17:12.000000000 +0200
@@ -61,6 +61,27 @@
 
 var _ gitService = (*git.Service)(nil)
 
+// IsRepoLocal reports whether worktreeDir is inside mainWorktreePath,
+// indicating that worktrees are stored within the repository itself.
+func IsRepoLocal(worktreeDir, mainWorktreePath string) bool {
+       if mainWorktreePath == "" || worktreeDir == "" {
+               return false
+       }
+       return 
strings.HasPrefix(filepath.Clean(worktreeDir)+string(filepath.Separator),
+               filepath.Clean(mainWorktreePath)+string(filepath.Separator))
+}
+
+// resolveWorktreeBaseDir returns the parent directory for a new worktree.
+// When worktreeDir is already inside mainWorktreePath (repo-local mode), the
+// repoName segment is omitted — the dir is already scoped to one repo.
+// Otherwise the classic <worktreeDir>/<repoName> structure is used.
+func resolveWorktreeBaseDir(worktreeDir, mainWorktreePath, repoName string) 
string {
+       if IsRepoLocal(worktreeDir, mainWorktreePath) {
+               return worktreeDir
+       }
+       return filepath.Join(worktreeDir, repoName)
+}
+
 // CreateFromBranch creates a worktree from a branch name.
 func CreateFromBranch(ctx context.Context, gitSvc gitService, cfg 
*config.AppConfig, branchName, worktreeName string, withChange, silent bool) 
(string, error) {
        return CreateFromBranchWithFS(ctx, gitSvc, cfg, branchName, 
worktreeName, withChange, silent, DefaultFS)
@@ -92,13 +113,14 @@
        }
 
        // Construct target path based on worktree name
+       mainWorktreePath := gitSvc.GetMainWorktreePath(ctx)
        repoName := gitSvc.ResolveRepoName(ctx)
 
        // Generate random name if not provided, or validate user-provided name
        if worktreeName == "" {
                // Generate random name with retry for uniqueness
                sanitizedBranch := utils.SanitizeBranchName(branchName, 50)
-               worktreeName = generateUniqueWorktreeNameFS(cfg, repoName, 
sanitizedBranch, fs)
+               worktreeName = generateUniqueWorktreeNameFS(cfg, 
mainWorktreePath, repoName, sanitizedBranch, fs)
        } else {
                // Validate and sanitise user-provided name
                sanitised := utils.SanitizeBranchName(worktreeName, 100)
@@ -108,7 +130,8 @@
                worktreeName = sanitised
        }
 
-       targetPath := filepath.Join(cfg.WorktreeDir, repoName, worktreeName)
+       base := resolveWorktreeBaseDir(cfg.WorktreeDir, mainWorktreePath, 
repoName)
+       targetPath := filepath.Join(base, worktreeName)
 
        // Check for path conflicts
        if _, err := fs.Stat(targetPath); err == nil {
@@ -190,13 +213,14 @@
 // generateUniqueWorktreeNameFS generates a unique worktree name with retries.
 // Format: <branch>-<random-adjective>-<random-noun>
 // Retries up to 10 times if path already exists.
-func generateUniqueWorktreeNameFS(cfg *config.AppConfig, repoName, branchName 
string, fs OSFilesystem) string {
+func generateUniqueWorktreeNameFS(cfg *config.AppConfig, mainWorktreePath, 
repoName, branchName string, fs OSFilesystem) string {
        const maxRetries = 10
 
+       base := resolveWorktreeBaseDir(cfg.WorktreeDir, mainWorktreePath, 
repoName)
        for range maxRetries {
                randomPart := utils.RandomBranchName()
                candidate := fmt.Sprintf("%s-%s", branchName, randomPart)
-               targetPath := filepath.Join(cfg.WorktreeDir, repoName, 
candidate)
+               targetPath := filepath.Join(base, candidate)
 
                // Check if path exists
                if _, err := fs.Stat(targetPath); os.IsNotExist(err) {
@@ -261,6 +285,7 @@
                return "", fmt.Errorf("branch %q is already checked out in 
worktree %q", localBranch, worktreePath)
        }
 
+       mainWorktreePath := gitSvc.GetMainWorktreePath(ctx)
        repoName := gitSvc.ResolveRepoName(ctx)
 
        if noWorkspace {
@@ -271,7 +296,8 @@
        }
 
        // Construct target path
-       targetPath := filepath.Join(cfg.WorktreeDir, repoName, worktreeName)
+       base := resolveWorktreeBaseDir(cfg.WorktreeDir, mainWorktreePath, 
repoName)
+       targetPath := filepath.Join(base, worktreeName)
 
        // Check for path conflicts
        if _, err := fs.Stat(targetPath); err == nil {
@@ -390,8 +416,10 @@
        }
 
        // Construct target path
+       mainWorktreePath := gitSvc.GetMainWorktreePath(ctx)
        repoName := gitSvc.ResolveRepoName(ctx)
-       targetPath := filepath.Join(cfg.WorktreeDir, repoName, branchName)
+       base := resolveWorktreeBaseDir(cfg.WorktreeDir, mainWorktreePath, 
repoName)
+       targetPath := filepath.Join(base, branchName)
 
        // Check for path conflicts
        if _, err := fs.Stat(targetPath); err == nil {
@@ -456,7 +484,7 @@
        }
 
        // Find the worktree to delete
-       selectedWorktree, err := FindWorktreeByPathOrName(worktreePath, 
nonMainWorktrees, cfg.WorktreeDir, gitSvc.ResolveRepoName(ctx))
+       selectedWorktree, err := FindWorktreeByPathOrName(worktreePath, 
nonMainWorktrees, cfg.WorktreeDir, gitSvc.ResolveRepoName(ctx), 
gitSvc.GetMainWorktreePath(ctx))
        if err != nil {
                return err
        }
@@ -538,7 +566,7 @@
                return fmt.Errorf("invalid new name: must contain at least one 
alphanumeric character")
        }
 
-       selectedWorktree, err := FindWorktreeByPathOrName(worktreePath, 
nonMainWorktrees, cfg.WorktreeDir, gitSvc.ResolveRepoName(ctx))
+       selectedWorktree, err := FindWorktreeByPathOrName(worktreePath, 
nonMainWorktrees, cfg.WorktreeDir, gitSvc.ResolveRepoName(ctx), 
gitSvc.GetMainWorktreePath(ctx))
        if err != nil {
                return err
        }
@@ -567,7 +595,7 @@
 }
 
 // FindWorktreeByPathOrName finds a worktree by its path or name.
-func FindWorktreeByPathOrName(pathOrName string, worktrees 
[]*models.WorktreeInfo, worktreeDir, repoName string) (*models.WorktreeInfo, 
error) {
+func FindWorktreeByPathOrName(pathOrName string, worktrees 
[]*models.WorktreeInfo, worktreeDir, repoName, mainWorktreePath string) 
(*models.WorktreeInfo, error) {
        // Try to match by exact path
        for _, wt := range worktrees {
                if wt.Path == pathOrName {
@@ -589,8 +617,8 @@
                }
        }
 
-       // Try to construct the path from worktree name and match
-       constructedPath := filepath.Join(worktreeDir, repoName, pathOrName)
+       // Try to construct the path from worktree name and match (handles both 
global and repo-local modes)
+       constructedPath := filepath.Join(resolveWorktreeBaseDir(worktreeDir, 
mainWorktreePath, repoName), pathOrName)
        for _, wt := range worktrees {
                if wt.Path == constructedPath {
                        return wt, nil
@@ -922,7 +950,7 @@
                }
        } else {
                repoName := gitSvc.ResolveRepoName(ctx)
-               target, err = FindWorktreeByPathOrName(worktreePathOrName, 
worktrees, cfg.WorktreeDir, repoName)
+               target, err = FindWorktreeByPathOrName(worktreePathOrName, 
worktrees, cfg.WorktreeDir, repoName, gitSvc.GetMainWorktreePath(ctx))
                if err != nil {
                        return nil, err
                }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lazyworktree-1.45.0/internal/cli/operations_test.go 
new/lazyworktree-1.45.1/internal/cli/operations_test.go
--- old/lazyworktree-1.45.0/internal/cli/operations_test.go     2026-03-28 
18:26:04.000000000 +0100
+++ new/lazyworktree-1.45.1/internal/cli/operations_test.go     2026-04-28 
15:17:12.000000000 +0200
@@ -221,7 +221,7 @@
        for _, tt := range tests {
                tt := tt
                t.Run(tt.name, func(t *testing.T) {
-                       found, err := FindWorktreeByPathOrName(tt.pathOrName, 
worktrees, worktreeDir, repoName)
+                       found, err := FindWorktreeByPathOrName(tt.pathOrName, 
worktrees, worktreeDir, repoName, "")
                        if tt.wantErr {
                                if err == nil {
                                        t.Fatalf("expected error")
@@ -238,6 +238,142 @@
        }
 }
 
+func TestResolveWorktreeBaseDir(t *testing.T) {
+       t.Parallel()
+
+       globalWorktreeDir := filepath.Join(string(filepath.Separator), "home", 
"user", ".local", "share", "worktrees")
+       mainWorktreePath := filepath.Join(string(filepath.Separator), "home", 
"user", "code", "myrepo")
+       repoLocalWorktreeDir := filepath.Join(mainWorktreePath, ".worktrees")
+       worktreesRootDir := filepath.Join(string(filepath.Separator), 
"worktrees")
+       similarPrefixWorktreeDir := filepath.Join(string(filepath.Separator), 
"home", "user", "code", "myrepo-other", ".worktrees")
+
+       tests := []struct {
+               name             string
+               worktreeDir      string
+               mainWorktreePath string
+               repoName         string
+               want             string
+       }{
+               {
+                       name:             "global mode: absolute dir outside 
repo",
+                       worktreeDir:      globalWorktreeDir,
+                       mainWorktreePath: mainWorktreePath,
+                       repoName:         "org-myrepo",
+                       want:             filepath.Join(globalWorktreeDir, 
"org-myrepo"),
+               },
+               {
+                       name:             "repo-local mode: dir inside main 
worktree",
+                       worktreeDir:      repoLocalWorktreeDir,
+                       mainWorktreePath: mainWorktreePath,
+                       repoName:         "org-myrepo",
+                       want:             repoLocalWorktreeDir,
+               },
+               {
+                       name:             "repo-local mode: dir equals main 
worktree path",
+                       worktreeDir:      mainWorktreePath,
+                       mainWorktreePath: mainWorktreePath,
+                       repoName:         "org-myrepo",
+                       want:             mainWorktreePath,
+               },
+               {
+                       name:             "empty mainWorktreePath falls back to 
global",
+                       worktreeDir:      worktreesRootDir,
+                       mainWorktreePath: "",
+                       repoName:         "myrepo",
+                       want:             filepath.Join(worktreesRootDir, 
"myrepo"),
+               },
+               {
+                       name:             "similar prefix does not trigger 
repo-local",
+                       worktreeDir:      similarPrefixWorktreeDir,
+                       mainWorktreePath: mainWorktreePath,
+                       repoName:         "org-myrepo",
+                       want:             
filepath.Join(similarPrefixWorktreeDir, "org-myrepo"),
+               },
+       }
+
+       for _, tt := range tests {
+               tt := tt
+               t.Run(tt.name, func(t *testing.T) {
+                       t.Parallel()
+                       got := resolveWorktreeBaseDir(tt.worktreeDir, 
tt.mainWorktreePath, tt.repoName)
+                       assert.Equal(t, tt.want, got)
+               })
+       }
+}
+
+func TestIsRepoLocal(t *testing.T) {
+       t.Parallel()
+
+       mainWorktreePath := filepath.Join(string(filepath.Separator), "home", 
"user", "code", "myrepo")
+
+       tests := []struct {
+               name             string
+               worktreeDir      string
+               mainWorktreePath string
+               want             bool
+       }{
+               {
+                       name:             "inside main worktree",
+                       worktreeDir:      filepath.Join(mainWorktreePath, 
".worktrees"),
+                       mainWorktreePath: mainWorktreePath,
+                       want:             true,
+               },
+               {
+                       name:             "equals main worktree path",
+                       worktreeDir:      mainWorktreePath,
+                       mainWorktreePath: mainWorktreePath,
+                       want:             true,
+               },
+               {
+                       name:             "outside main worktree",
+                       worktreeDir:      
filepath.Join(string(filepath.Separator), "home", "user", ".local", "share", 
"worktrees"),
+                       mainWorktreePath: mainWorktreePath,
+                       want:             false,
+               },
+               {
+                       name:             "similar prefix is not repo-local",
+                       worktreeDir:      
filepath.Join(string(filepath.Separator), "home", "user", "code", 
"myrepo-other", ".worktrees"),
+                       mainWorktreePath: mainWorktreePath,
+                       want:             false,
+               },
+               {
+                       name:             "empty mainWorktreePath",
+                       worktreeDir:      
filepath.Join(string(filepath.Separator), "worktrees"),
+                       mainWorktreePath: "",
+                       want:             false,
+               },
+               {
+                       name:             "empty worktreeDir",
+                       worktreeDir:      "",
+                       mainWorktreePath: mainWorktreePath,
+                       want:             false,
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       t.Parallel()
+                       got := IsRepoLocal(tt.worktreeDir, tt.mainWorktreePath)
+                       assert.Equal(t, tt.want, got)
+               })
+       }
+}
+
+func TestFindWorktreeByPathOrNameRepoLocal(t *testing.T) {
+       t.Parallel()
+
+       mainPath := filepath.Join(string(filepath.Separator), "home", "user", 
"code", "myrepo")
+       worktreeDir := filepath.Join(mainPath, ".worktrees")
+       repoName := "org-myrepo"
+
+       wtFeature := &models.WorktreeInfo{Path: filepath.Join(worktreeDir, 
"feature"), Branch: "feature"}
+       worktrees := []*models.WorktreeInfo{wtFeature}
+
+       found, err := FindWorktreeByPathOrName("feature", worktrees, 
worktreeDir, repoName, mainPath)
+       require.NoError(t, err)
+       assert.Equal(t, wtFeature, found)
+}
+
 func TestBranchExists(t *testing.T) {
        t.Parallel()
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lazyworktree-1.45.0/internal/config/config.go 
new/lazyworktree-1.45.1/internal/config/config.go
--- old/lazyworktree-1.45.0/internal/config/config.go   2026-03-28 
18:26:04.000000000 +0100
+++ new/lazyworktree-1.45.1/internal/config/config.go   2026-04-28 
15:17:12.000000000 +0200
@@ -65,6 +65,7 @@
        PRBranchNameTemplate    string // Template for PR branch names with 
placeholders: {number}, {title}, {generated}, {pr_author} (default: 
"pr-{number}-{title}")
        SessionPrefix           string // Prefix for tmux/zellij session names 
(default: "wt-")
        Layout                  string // Pane arrangement: "default" or "top" 
(default: "default")
+       PruneStaleBranches      bool   // Include merged branches without 
worktrees in prune (default: false)
        PaletteMRU              bool   // Enable MRU sorting for command 
palette (default: false)
        PaletteMRULimit         int    // Number of MRU items to show (default: 
5)
        AgentSessionClaudeRoot  string // Custom root for Claude transcript 
discovery (default: ~/.claude/projects)
@@ -236,6 +237,7 @@
        cfg.RefreshIntervalSeconds = coerceInt(data["refresh_interval"], 
cfg.RefreshIntervalSeconds)
        cfg.SearchAutoSelect = coerceBool(data["search_auto_select"], false)
        cfg.FuzzyFinderInput = coerceBool(data["fuzzy_finder_input"], false)
+       cfg.PruneStaleBranches = coerceBool(data["prune_stale_branches"], false)
 
        if iconSet, ok := data["icon_set"].(string); ok {
                iconSet = strings.ToLower(strings.TrimSpace(iconSet))
@@ -629,6 +631,9 @@
        if _, ok := overrideData["fuzzy_finder_input"]; ok {
                cfg.FuzzyFinderInput = overrideCfg.FuzzyFinderInput
        }
+       if _, ok := overrideData["prune_stale_branches"]; ok {
+               cfg.PruneStaleBranches = overrideCfg.PruneStaleBranches
+       }
        if _, ok := overrideData["icon_set"]; ok {
                cfg.IconSet = overrideCfg.IconSet
        }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lazyworktree-1.45.0/internal/config/config_test.go 
new/lazyworktree-1.45.1/internal/config/config_test.go
--- old/lazyworktree-1.45.0/internal/config/config_test.go      2026-03-28 
18:26:04.000000000 +0100
+++ new/lazyworktree-1.45.1/internal/config/config_test.go      2026-04-28 
15:17:12.000000000 +0200
@@ -462,6 +462,22 @@
                        },
                },
                {
+                       name: "prune_stale_branches true",
+                       data: map[string]interface{}{
+                               "prune_stale_branches": true,
+                       },
+                       validate: func(t *testing.T, cfg *AppConfig) {
+                               assert.True(t, cfg.PruneStaleBranches)
+                       },
+               },
+               {
+                       name: "prune_stale_branches false by default",
+                       data: map[string]interface{}{},
+                       validate: func(t *testing.T, cfg *AppConfig) {
+                               assert.False(t, cfg.PruneStaleBranches)
+                       },
+               },
+               {
                        name: "search_auto_select true",
                        data: map[string]interface{}{
                                "search_auto_select": true,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lazyworktree-1.45.0/internal/config/gitconfig.go 
new/lazyworktree-1.45.1/internal/config/gitconfig.go
--- old/lazyworktree-1.45.0/internal/config/gitconfig.go        2026-03-28 
18:26:04.000000000 +0100
+++ new/lazyworktree-1.45.1/internal/config/gitconfig.go        2026-04-28 
15:17:12.000000000 +0200
@@ -54,7 +54,7 @@
                        continue
                }
 
-               key := strings.TrimPrefix(parts[0], "lw.")
+               key := strings.ReplaceAll(strings.TrimPrefix(parts[0], "lw."), 
"-", "_")
                value := parts[1]
 
                // Git config can have multi-values for same key
@@ -111,13 +111,11 @@
 
 // loadGitConfig reads git config values and returns map for parseConfig.
 func loadGitConfig(globalOnly bool, repoPath string) (map[string]any, error) {
-       args := []string{"config", "--get-regexp", "^lw\\."}
-
+       scope := "--local"
        if globalOnly {
-               args = append(args, "--global")
-       } else {
-               args = append(args, "--local")
+               scope = "--global"
        }
+       args := []string{"config", scope, "--get-regexp", "^lw\\."}
 
        output, err := runGitConfig(args, repoPath)
        if err != nil {
@@ -182,7 +180,7 @@
                        return nil, fmt.Errorf("config override key must start 
with 'lw.': %q", fullKey)
                }
 
-               key := strings.TrimPrefix(fullKey, "lw.")
+               key := strings.ReplaceAll(strings.TrimPrefix(fullKey, "lw."), 
"-", "_")
                if key == "" {
                        return nil, fmt.Errorf("empty config key in override: 
%q", override)
                }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/lazyworktree-1.45.0/internal/config/gitconfig_test.go 
new/lazyworktree-1.45.1/internal/config/gitconfig_test.go
--- old/lazyworktree-1.45.0/internal/config/gitconfig_test.go   2026-03-28 
18:26:04.000000000 +0100
+++ new/lazyworktree-1.45.1/internal/config/gitconfig_test.go   2026-04-28 
15:17:12.000000000 +0200
@@ -59,6 +59,17 @@
                        expected: map[string][]string{},
                },
                {
+                       name: "hyphens normalised to underscores",
+                       output: `lw.worktree-dir /path/to/dir
+lw.auto-fetch-prs true
+lw.init-commands npm install`,
+                       expected: map[string][]string{
+                               "worktree_dir":   {"/path/to/dir"},
+                               "auto_fetch_prs": {"true"},
+                               "init_commands":  {"npm install"},
+                       },
+               },
+               {
                        name: "mixed valid and empty lines",
                        output: `lw.theme nord
 
@@ -240,6 +251,14 @@
                        errMsg:    "empty config key",
                },
                {
+                       name:      "hyphenated keys normalised",
+                       overrides: []string{"lw.worktree-dir=/path", 
"lw.auto-fetch-prs=true"},
+                       expected: map[string]any{
+                               "worktree_dir":   "/path",
+                               "auto_fetch_prs": "true",
+                       },
+               },
+               {
                        name:      "empty value is allowed",
                        overrides: []string{"lw.theme="},
                        expected: map[string]any{
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lazyworktree-1.45.0/internal/git/service_host.go 
new/lazyworktree-1.45.1/internal/git/service_host.go
--- old/lazyworktree-1.45.0/internal/git/service_host.go        2026-03-28 
18:26:04.000000000 +0100
+++ new/lazyworktree-1.45.1/internal/git/service_host.go        2026-04-28 
15:17:12.000000000 +0200
@@ -5,6 +5,7 @@
        "crypto/sha256"
        "encoding/json"
        "fmt"
+       "net/url"
        "regexp"
        "strings"
 )
@@ -104,7 +105,11 @@
                return "unknown"
        }
 
-       return strings.TrimSuffix(repoName, ".git")
+       repoName = strings.TrimSuffix(repoName, ".git")
+       if decoded, err := url.PathUnescape(repoName); err == nil {
+               repoName = decoded
+       }
+       return repoName
 }
 
 // localRepoKey builds a stable, compact cache key when no remote name is 
available.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/lazyworktree-1.45.0/internal/git/service_host_test.go 
new/lazyworktree-1.45.1/internal/git/service_host_test.go
--- old/lazyworktree-1.45.0/internal/git/service_host_test.go   2026-03-28 
18:26:04.000000000 +0100
+++ new/lazyworktree-1.45.1/internal/git/service_host_test.go   2026-04-28 
15:17:12.000000000 +0200
@@ -127,6 +127,29 @@
                assert.Equal(t, "owner/repo", 
service.ResolveRepoName(context.Background()))
        })
 
+       t.Run("url-encoded remote segments are decoded", func(t *testing.T) {
+               cases := []struct {
+                       name   string
+                       remote string
+                       want   string
+               }{
+                       {name: "space in org", remote: 
"https://gitea.example.com/Company%20A/myrepo.git";, want: "Company A/myrepo"},
+                       {name: "encoded slash in github org", remote: 
"https://github.com/org%2Fname/repo.git";, want: "org/name/repo"},
+                       {name: "no encoding", remote: 
"[email protected]:owner/repo.git", want: "owner/repo"},
+               }
+               for _, tc := range cases {
+                       t.Run(tc.name, func(t *testing.T) {
+                               repo := t.TempDir()
+                               runGit(t, repo, "init")
+                               runGit(t, repo, "remote", "add", "origin", 
tc.remote)
+                               withCwd(t, repo)
+
+                               service := NewService(func(string, string) {}, 
func(string, string, string) {})
+                               assert.Equal(t, tc.want, 
service.ResolveRepoName(context.Background()))
+                       })
+               }
+       })
+
        t.Run("resolve local key when no remote is configured", func(t 
*testing.T) {
                tmpDir := t.TempDir()
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lazyworktree-1.45.0/lazyworktree.1 
new/lazyworktree-1.45.1/lazyworktree.1
--- old/lazyworktree-1.45.0/lazyworktree.1      2026-03-28 18:26:04.000000000 
+0100
+++ new/lazyworktree-1.45.1/lazyworktree.1      2026-04-28 15:17:12.000000000 
+0200
@@ -66,6 +66,12 @@
 Override the default worktree root directory.
 .br
 Default: ~/.local/share/worktrees
+.br
+\fB$LWT_REPO_PATH\fR is automatically set to the git repository root, enabling
+repo-local placement: \fI$LWT_REPO_PATH/.worktrees\fR. When the directory is
+inside the repository, the \fI<repoName>\fR segment is omitted so worktrees
+land directly under the configured directory. Add the directory to
+\fI.gitignore\fR so it does not appear in \fBgit status\fR.
 .
 .TP
 .B \-\-theme \fINAME\fR
@@ -666,7 +672,7 @@
 .
 .TP
 .B X
-Prune merged worktrees. Automatically refreshes PR/MR data from GitHub or 
GitLab (if connected), then detects worktrees whose associated PR has been 
merged or whose branch has been merged into the main branch. For repositories 
without GitHub/GitLab remotes, uses git-based merge detection only. Displays a 
checklist allowing selection of which worktrees to remove.
+Prune merged worktrees and stale branches. Automatically refreshes PR/MR data 
from GitHub or GitLab (if connected), then detects worktrees whose associated 
PR has been merged or whose branch has been merged into the main branch. For 
repositories without GitHub/GitLab remotes, uses git-based merge detection 
only. When \fBprune_stale_branches\fR is enabled, also includes local branches 
that are merged but have no associated worktree. Displays a checklist allowing 
selection of which items to remove.
 .
 .TP
 .B !
@@ -1010,12 +1016,13 @@
 .nf
 # Set globally
 git config --global lw.theme nord
-git config --global lw.worktree_dir ~/.local/share/worktrees
+git config --global lw.worktree-dir ~/.local/share/worktrees
 
 # Set per-repository (overrides global)
 git config --local lw.theme dracula
-git config --local lw.init_commands "link_topsymlinks"
-git config --local lw.init_commands "npm install"  # Multi-values supported
+git config --local lw.worktree-dir '$LWT_REPO_PATH/.worktrees'  # Repo-local 
worktrees
+git config --local lw.init-commands "link_topsymlinks"
+git config --local lw.init-commands "npm install"  # Multi-values supported
 
 # View all lazyworktree settings
 git config --global --get-regexp "^lw\."

++++++ lazyworktree.obsinfo ++++++
--- /var/tmp/diff_new_pack.RcRVfx/_old  2026-04-30 20:30:40.739505767 +0200
+++ /var/tmp/diff_new_pack.RcRVfx/_new  2026-04-30 20:30:40.795508044 +0200
@@ -1,5 +1,5 @@
 name: lazyworktree
-version: 1.45.0
-mtime: 1774718764
-commit: 0d2cb7b5d6f1a57f66620ed34e02b3d9c996a5cd
+version: 1.45.1
+mtime: 1777382232
+commit: 931d43166ed25419350a6b2f7c4e2c5db0aeb91e
 

++++++ vendor.tar.gz ++++++
++++ 4475 lines of diff (skipped)

Reply via email to