diff --git a/plugins/README.md b/plugins/README.md
index cf4a21ecc5..77662c246b 100644
--- a/plugins/README.md
+++ b/plugins/README.md
@@ -16,6 +16,7 @@ Learn more in the [official plugins documentation](https://docs.claude.com/en/do
| [claude-opus-4-5-migration](./claude-opus-4-5-migration/) | Migrate code and prompts from Sonnet 4.x and Opus 4.1 to Opus 4.5 | **Skill:** `claude-opus-4-5-migration` - Automated migration of model strings, beta headers, and prompt adjustments |
| [code-review](./code-review/) | Automated PR code review using multiple specialized agents with confidence-based scoring to filter false positives | **Command:** `/code-review` - Automated PR review workflow
**Agents:** 5 parallel Sonnet agents for CLAUDE.md compliance, bug detection, historical context, PR history, and code comments |
| [commit-commands](./commit-commands/) | Git workflow automation for committing, pushing, and creating pull requests | **Commands:** `/commit`, `/commit-push-pr`, `/clean_gone` - Streamlined git operations |
+| [git-aware-history](./git-aware-history/) | Fix Claude Code session history fragmentation across git worktrees — all worktrees of a repo share one history directory | **Command:** `/consolidate-history` - End-to-end setup: installs PATH wrapper, migrates existing orphaned history
**Scripts:** `consolidate-git-history.sh` - Retroactive migration, `claude-wrapper.sh` - PATH wrapper template |
| [explanatory-output-style](./explanatory-output-style/) | Adds educational insights about implementation choices and codebase patterns (mimics the deprecated Explanatory output style) | **Hook:** SessionStart - Injects educational context at the start of each session |
| [feature-dev](./feature-dev/) | Comprehensive feature development workflow with a structured 7-phase approach | **Command:** `/feature-dev` - Guided feature development workflow
**Agents:** `code-explorer`, `code-architect`, `code-reviewer` - For codebase analysis, architecture design, and quality review |
| [frontend-design](./frontend-design/) | Create distinctive, production-grade frontend interfaces that avoid generic AI aesthetics | **Skill:** `frontend-design` - Auto-invoked for frontend work, providing guidance on bold design choices, typography, animations, and visual details |
diff --git a/plugins/git-aware-history/.claude-plugin/plugin.json b/plugins/git-aware-history/.claude-plugin/plugin.json
new file mode 100644
index 0000000000..c45608d42a
--- /dev/null
+++ b/plugins/git-aware-history/.claude-plugin/plugin.json
@@ -0,0 +1,9 @@
+{
+ "name": "git-aware-history",
+ "description": "Consolidate Claude Code session history across git worktrees — all worktrees of a repo share one history directory",
+ "version": "1.0.0",
+ "author": {
+ "name": "Ilan Peretz",
+ "email": "ilan.peretz@optibus.com"
+ }
+}
diff --git a/plugins/git-aware-history/README.md b/plugins/git-aware-history/README.md
new file mode 100644
index 0000000000..6f377f719a
--- /dev/null
+++ b/plugins/git-aware-history/README.md
@@ -0,0 +1,154 @@
+# Git-Aware History Plugin
+
+Fix Claude Code session history fragmentation across git worktrees.
+
+## The Problem
+
+Claude Code keys session history by the working directory path (`~/.claude/projects//`). When you use git worktrees, each worktree gets its own isolated history directory. Deleting a worktree orphans its history — `/resume` can't find it, and there's no way to see all sessions for a repo in one place.
+
+Active worktree users end up with dozens of orphaned history directories after a sprint.
+
+## The Solution
+
+`git rev-parse --git-common-dir` returns the **same** `.git` path for any worktree of a repo. This plugin uses that as the canonical project identity, so all worktrees of a repo share one history directory.
+
+## Components
+
+### 1. PATH wrapper script (ongoing routing)
+
+A small `claude` wrapper script installed to `~/.local/bin/claude` — **before** the real binary on PATH. It runs `git rev-parse --git-common-dir` on every invocation and symlinks the worktree's project dir to the git-root's project dir before Claude Code opens it.
+
+**No shell function, no alias, no `.zshrc` sourcing required. Gated by a flag file so you can disable/re-enable without touching the script:** Because it sits on PATH, it works in every shell (bash, zsh, fish), every terminal, and IDE integrations that don't load shell profiles. The real `claude` binary path is baked in at install time.
+
+```bash
+touch ~/.claude/git-aware-history.enabled # enable
+rm ~/.claude/git-aware-history.enabled # disable
+```
+
+
+### 2. Consolidation script (`consolidate-git-history.sh`)
+
+Retroactively migrates existing orphaned history. Two phases:
+
+- **Phase 1 — live paths**: finds all project dirs whose worktrees still exist, groups them by git root, merges and symlinks automatically
+- **Phase 2 — deleted paths**: interactively handles orphaned dirs from already-deleted worktrees. Shows metadata (session count, size, date range, branch names) per group. Supports `fzf --multi` or a numbered fallback menu.
+
+Always dry-runs first. Use `--execute` to apply.
+
+### 3. `/consolidate-history` command
+
+End-to-end setup in one command: installs the wrapper script to the right PATH location, migrates existing history with a dry-run preview + confirmation, and verifies everything is wired up.
+
+## Installation
+
+### Option A: via `/consolidate-history` command (recommended)
+
+If you have this plugin installed, just run:
+```
+/consolidate-history
+```
+
+Claude will:
+1. Find the real `claude` binary path
+2. Install the wrapper script to `~/.local/bin/claude`
+3. Verify `~/.local/bin` is on PATH before the real binary (and guide you to fix it if not)
+4. Migrate your existing history with a dry-run preview
+
+### Option B: manual
+
+1. Copy `consolidate-git-history.sh` to `~/.claude/scripts/` and make it executable:
+ ```bash
+ mkdir -p ~/.claude/scripts
+ cp consolidate-git-history.sh ~/.claude/scripts/
+ chmod +x ~/.claude/scripts/consolidate-git-history.sh
+ ```
+
+2. Install the PATH wrapper. First find your real `claude` binary:
+ ```bash
+ which claude # e.g. /opt/homebrew/bin/claude
+ ```
+
+ Then install the wrapper to `~/.local/bin/claude`, substituting the real path:
+ ```bash
+ mkdir -p ~/.local/bin
+ sed "s|REAL_CLAUDE|/opt/homebrew/bin/claude|" claude-wrapper.sh > ~/.local/bin/claude
+ chmod +x ~/.local/bin/claude
+ ```
+
+3. Ensure `~/.local/bin` is before the real binary on PATH. Add to your shell profile if needed:
+ ```bash
+ export PATH="$HOME/.local/bin:$PATH"
+ ```
+
+ Verify:
+ ```bash
+ which claude # should show ~/.local/bin/claude
+ which -a claude # should show ~/.local/bin/claude first, then the real binary
+ ```
+
+4. Migrate existing history:
+ ```bash
+ ~/.claude/scripts/consolidate-git-history.sh --dry-run
+ ~/.claude/scripts/consolidate-git-history.sh --execute
+ ```
+
+## Usage
+
+### Migrate existing history
+
+```bash
+# Dry-run (safe — no changes)
+~/.claude/scripts/consolidate-git-history.sh --dry-run
+
+# Apply
+~/.claude/scripts/consolidate-git-history.sh --execute
+```
+
+During `--execute`, Phase 2 prompts you interactively for each group of orphaned sessions:
+
+```
+Orphaned sessions — inferred repo: /Users/you/dev/myrepo (inferred)
+ [1] -...-feature-auth | 2 sessions | 1.2MB | Apr 28 | feature/auth
+ [2] -...-bugfix-login | 1 session | 340KB | Apr 15 | bugfix/login
+
+Merge into: -Users-you-dev-myrepo?
+ [a] all [n] none [1,2,...] pick numbers [s] skip group
+>
+```
+
+### Test safely with a temp directory
+
+```bash
+PROJECTS_DIR=$(mktemp -d) bash ~/.claude/scripts/consolidate-git-history.sh --execute
+```
+
+## How It Works
+
+The slug algorithm matches Claude Code's existing behaviour: replace every `/` and `.` in the path with `-`. The wrapper computes slugs for both the current CWD and the git root — if they differ (you're in a worktree), it creates a symlink `~/.claude/projects/` → `~/.claude/projects/` using `ln -sfn` (the `-n` flag prevents nesting inside an existing directory).
+
+## Compatibility
+
+- macOS (bash 3.2+) and Linux
+- No dependencies beyond `git`, `python3` (ships with macOS), and optionally `fzf` for better interactive UX
+- `fzf` is used automatically when available and stdin is a TTY; falls back to a numbered text menu otherwise
+
+## Upstream Fix
+
+The root cause is that Claude Code uses the raw CWD as the project key instead of the git repo root. The minimal upstream fix is one function in Claude Code's project-path computation:
+
+```typescript
+async function resolveProjectRoot(cwd: string): Promise {
+ try {
+ const { stdout } = await execFileAsync('git', ['rev-parse', '--git-common-dir'], { cwd });
+ const gitCommonDir = stdout.trim();
+ const absCommonDir = path.isAbsolute(gitCommonDir)
+ ? gitCommonDir
+ : path.join(cwd, gitCommonDir);
+ return path.dirname(absCommonDir);
+ } catch {
+ return cwd;
+ }
+}
+```
+
+This plugin is the userland workaround until that change lands. Tracked in [anthropics/claude-code#52113](https://github.com/anthropics/claude-code/issues/52113).
diff --git a/plugins/git-aware-history/claude-wrapper.sh b/plugins/git-aware-history/claude-wrapper.sh
new file mode 100755
index 0000000000..8a7277e50c
--- /dev/null
+++ b/plugins/git-aware-history/claude-wrapper.sh
@@ -0,0 +1,25 @@
+#!/usr/bin/env bash
+# Git-aware Claude Code wrapper
+# Installed by: https://github.com/ilanp-ob/claude-git-aware-history
+#
+# Disable without uninstalling:
+# rm ~/.claude/git-aware-history.enabled
+# Re-enable:
+# touch ~/.claude/git-aware-history.enabled
+
+if [[ -f "$HOME/.claude/git-aware-history.enabled" ]]; then
+ if git_common=$(git rev-parse --git-common-dir 2>/dev/null); then
+ [[ "$git_common" != /* ]] && git_common="$PWD/$git_common"
+ git_root="${git_common%/.git}"
+ cwd_slug="${PWD//[\/.]/-}"
+ root_slug="${git_root//[\/.]/-}"
+
+ if [[ "$cwd_slug" != "$root_slug" ]]; then
+ projects_dir="$HOME/.claude/projects"
+ mkdir -p "$projects_dir/$root_slug"
+ ln -sfn "$projects_dir/$root_slug" "$projects_dir/$cwd_slug"
+ fi
+ fi
+fi
+
+exec REAL_CLAUDE "$@"
diff --git a/plugins/git-aware-history/commands/consolidate-history.md b/plugins/git-aware-history/commands/consolidate-history.md
new file mode 100644
index 0000000000..c7f2c86d6a
--- /dev/null
+++ b/plugins/git-aware-history/commands/consolidate-history.md
@@ -0,0 +1,77 @@
+---
+allowed-tools: Bash(ls:*), Bash(which:*), Bash(grep:*), Bash(mkdir:*), Bash(chmod:*), Bash(echo:*), Bash(cat:*), Bash(sed:*), Write
+description: Consolidate Claude Code session history across git worktrees into a single per-repo directory
+---
+
+# Consolidate Git History
+
+Merge Claude Code session history from worktrees into a single per-repo directory, and install the PATH wrapper for ongoing routing.
+
+## When invoked
+
+Follow these steps in order. Do not skip steps.
+
+## Step 1: Check for the consolidation script
+
+Check whether `~/.claude/scripts/consolidate-git-history.sh` exists:
+
+```bash
+ls -la ~/.claude/scripts/consolidate-git-history.sh 2>/dev/null || echo "NOT FOUND"
+```
+
+If NOT FOUND, install it: create `~/.claude/scripts/` if needed, then write the full content of `consolidate-git-history.sh` from this plugin directory to `~/.claude/scripts/consolidate-git-history.sh` and `chmod +x` it.
+
+## Step 2: Run dry-run and show the plan
+
+```bash
+echo "n" | ~/.claude/scripts/consolidate-git-history.sh --dry-run
+```
+
+(Pipe "n" to skip interactive prompts during the preview.) Show the full output to the user. Ask: "Does this look right? Shall I run with --execute to apply?"
+
+## Step 3: Run with --execute (after confirmation)
+
+Only proceed if the user confirms. Then run interactively (no piped input — the user will answer the Phase 2 prompts themselves):
+
+```bash
+~/.claude/scripts/consolidate-git-history.sh --execute
+```
+
+Show the full output.
+
+## Step 4: Install the PATH wrapper script
+
+This is the preferred approach — it works in every shell and IDE without sourcing `.zshrc`.
+
+First check if the wrapper is already installed:
+
+```bash
+ls -la ~/.local/bin/claude 2>/dev/null || echo "NOT INSTALLED"
+which -a claude 2>/dev/null
+```
+
+If NOT INSTALLED, install it:
+
+1. Find the real `claude` binary (the one that is NOT `~/.local/bin/claude`):
+ ```bash
+ which -a claude | grep -v "$HOME/.local/bin/claude" | head -1
+ ```
+
+2. Write the wrapper script, substituting the real binary path:
+ ```bash
+ mkdir -p ~/.local/bin
+ sed "s|REAL_CLAUDE||" /claude-wrapper.sh > ~/.local/bin/claude
+ chmod +x ~/.local/bin/claude
+ ```
+
+3. Verify `~/.local/bin` comes before the real binary on PATH:
+ ```bash
+ which claude # should show ~/.local/bin/claude
+ ```
+
+ If `which claude` still shows the old binary, `~/.local/bin` is not first on PATH. Tell the user to add this line to their shell profile and reload:
+ ```bash
+ export PATH="$HOME/.local/bin:$PATH"
+ ```
+
+Once installed, every `claude` invocation (including `c=claude` aliases and IDE integrations) automatically routes worktree sessions to the correct git-root project directory.
diff --git a/plugins/git-aware-history/consolidate-git-history.sh b/plugins/git-aware-history/consolidate-git-history.sh
new file mode 100755
index 0000000000..2f8cac1f6f
--- /dev/null
+++ b/plugins/git-aware-history/consolidate-git-history.sh
@@ -0,0 +1,426 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+PROJECTS_DIR="${PROJECTS_DIR:-$HOME/.claude/projects}"
+DRY_RUN=true
+
+# ── Argument parsing ────────────────────────────────────────────────────────
+for arg in "$@"; do
+ case "$arg" in
+ --execute) DRY_RUN=false ;;
+ --dry-run) DRY_RUN=true ;;
+ --help|-h)
+ echo "Usage: consolidate-git-history.sh [--dry-run|--execute]"
+ echo ""
+ echo " --dry-run (default) Show what would be merged without changing anything"
+ echo " --execute Perform the merge and replace source dirs with symlinks"
+ exit 0
+ ;;
+ *) echo "Unknown argument: $arg" >&2; exit 1 ;;
+ esac
+done
+
+$DRY_RUN && echo "[DRY RUN] Pass --execute to apply changes." || echo "[EXECUTE] Changes will be applied."
+echo ""
+
+# ── Utilities ────────────────────────────────────────────────────────────────
+
+# Slugify a path: replace every / and . with -
+slugify() {
+ echo "${1//[\/.]/-}"
+}
+
+# Extract all unique cwd values from JSONL files in a project dir
+extract_cwds() {
+ local dir="$1"
+ python3 - "$dir" <<'PYEOF'
+import sys, json, glob, os
+
+project_dir = sys.argv[1]
+cwds = set()
+for f in glob.glob(os.path.join(project_dir, "*.jsonl")):
+ with open(f, errors="replace") as fh:
+ for line in fh:
+ line = line.strip()
+ if not line:
+ continue
+ try:
+ d = json.loads(line)
+ cwd = d.get("cwd")
+ if cwd:
+ cwds.add(cwd)
+ except json.JSONDecodeError:
+ pass
+for c in sorted(cwds):
+ print(c)
+PYEOF
+}
+
+# Collect metadata for a project dir: session count, size, date range, branches
+collect_metadata() {
+ local dir="$1"
+ python3 - "$dir" <<'PYEOF'
+import sys, json, glob, os
+
+project_dir = sys.argv[1]
+sessions = set()
+branches = set()
+timestamps = []
+total_bytes = 0
+
+for f in glob.glob(os.path.join(project_dir, "*.jsonl")):
+ total_bytes += os.path.getsize(f)
+ with open(f, errors="replace") as fh:
+ for line in fh:
+ line = line.strip()
+ if not line:
+ continue
+ try:
+ d = json.loads(line)
+ if d.get("sessionId"):
+ sessions.add(d["sessionId"])
+ if d.get("gitBranch"):
+ branches.add(d["gitBranch"])
+ if d.get("timestamp"):
+ timestamps.append(d["timestamp"])
+ except json.JSONDecodeError:
+ pass
+
+size_mb = total_bytes / (1024 * 1024)
+size_str = f"{size_mb:.1f}MB" if size_mb >= 0.1 else f"{total_bytes}B"
+date_str = ""
+if timestamps:
+ timestamps.sort()
+ from datetime import datetime
+ try:
+ first = datetime.fromisoformat(timestamps[0].replace("Z", "+00:00"))
+ last = datetime.fromisoformat(timestamps[-1].replace("Z", "+00:00"))
+ date_str = f"{first.strftime('%b %d')} - {last.strftime('%b %d')}"
+ except Exception:
+ date_str = timestamps[-1][:10]
+
+branch_str = ", ".join(sorted(branches)[:3])
+if len(branches) > 3:
+ branch_str += f" (+{len(branches)-3} more)"
+
+print(f"{len(sessions)} sessions|{size_str}|{date_str}|{branch_str}")
+PYEOF
+}
+
+# Given a path that no longer exists, walk up ancestor dirs to find
+# the nearest existing git repo root. Prints the path and returns 0 if found.
+infer_git_root_for_deleted_path() {
+ local path="$1"
+ [[ -z "$path" ]] && return 1
+ local candidate="$path"
+ while true; do
+ if [[ -d "$candidate/.git" || -f "$candidate/.git" ]]; then
+ echo "$candidate"
+ return 0
+ fi
+ [[ "$candidate" == "/" ]] && break
+ local next
+ next=$(dirname "$candidate")
+ [[ "$next" == "$candidate" ]] && break # dirname returned same path, stop
+ candidate="$next"
+ done
+ return 1
+}
+
+# Present an interactive multi-select for a group of orphaned project dirs.
+# Prints selected 1-based indices to stdout, one per line.
+interactive_select() {
+ local prompt="$1"
+ shift
+ local items=("$@")
+
+ # Use fzf only when stdin is a TTY (interactive terminal)
+ if command -v fzf &>/dev/null && [[ -t 0 ]]; then
+ printf '%s\n' "${items[@]}" \
+ | fzf --multi --prompt="$prompt > " --header="Tab to select, Enter to confirm" \
+ | while IFS= read -r selected; do
+ for i in "${!items[@]}"; do
+ [[ "${items[$i]}" == "$selected" ]] && echo "$((i+1))"
+ done
+ done
+ else
+ # Display goes to stderr so stdout is clean for the index output
+ {
+ echo ""
+ echo "$prompt"
+ for i in "${!items[@]}"; do
+ printf " [%d] %s\n" "$((i+1))" "${items[$i]}"
+ done
+ echo ""
+ echo " [a] all [n] none [1,2,...] pick numbers [s] skip group"
+ printf "> "
+ } >&2
+ local choice
+ read -r choice || choice=""
+ case "$choice" in
+ a|A) for i in "${!items[@]}"; do echo "$((i+1))"; done ;;
+ n|N|s|S) ;;
+ *)
+ local picks_str="$choice"
+ local pick
+ while IFS=',' read -r pick; do
+ pick=$(echo "$pick" | tr -d ' ')
+ [[ "$pick" =~ ^[0-9]+$ ]] && echo "$pick"
+ done <<< "$picks_str"
+ ;;
+ esac
+ fi
+}
+
+# Merge all JSONL files and subagent dirs from src_dir into dst_dir.
+# Replaces src_dir with a symlink to dst_dir if --execute.
+merge_into() {
+ local src_dir="$1"
+ local dst_dir="$2"
+
+ # Copy JSONL session files
+ for f in "$src_dir"/*.jsonl; do
+ [[ -e "$f" ]] || continue
+ local fname
+ fname=$(basename "$f")
+ if [[ -e "$dst_dir/$fname" ]]; then
+ echo " WARNING: collision on $fname — skipping" >&2
+ continue
+ fi
+ if $DRY_RUN; then
+ echo " copy $fname"
+ else
+ cp "$f" "$dst_dir/$fname"
+ fi
+ done
+
+ # Copy subagent subdirectories (named by session UUID)
+ for d in "$src_dir"/*/; do
+ [[ -d "$d" ]] || continue
+ local dname
+ dname=$(basename "$d")
+ if [[ -e "$dst_dir/$dname" ]]; then
+ echo " WARNING: collision on subdir $dname — skipping" >&2
+ continue
+ fi
+ if $DRY_RUN; then
+ echo " copy subdir $dname/"
+ else
+ cp -r "$d" "$dst_dir/$dname"
+ fi
+ done
+
+ # Replace source dir with symlink
+ if $DRY_RUN; then
+ echo " replace $(basename "$src_dir") with symlink -> $(basename "$dst_dir")"
+ else
+ local src_name
+ src_name=$(basename "$src_dir")
+ [[ "$src_dir" == "$PROJECTS_DIR/"* ]] || { echo "ERROR: refusing to remove $src_dir (not under $PROJECTS_DIR)" >&2; return 1; }
+ rm -rf "$src_dir"
+ ln -sfn "$dst_dir" "$PROJECTS_DIR/$src_name"
+ echo " Merged and linked: $src_name -> $(basename "$dst_dir")"
+ fi
+}
+
+# ── Phase 1: Live paths ──────────────────────────────────────────────────────
+echo "=== Phase 1: Live worktree paths ==="
+echo ""
+
+# Parallel indexed arrays (bash 3.2-compatible; no declare -A)
+root_slugs=() # unique root slugs
+root_members=() # space-separated project dir names per slug
+root_paths_arr=() # git root absolute path per slug
+
+# Returns the index of a slug in root_slugs, or exits 1 if not found
+_find_slug_idx() {
+ local target="$1" i
+ for i in "${!root_slugs[@]}"; do
+ [[ "${root_slugs[$i]}" == "$target" ]] && { echo "$i"; return 0; }
+ done
+ return 1
+}
+
+for project_dir in "$PROJECTS_DIR"/*/; do
+ [[ -d "$project_dir" ]] || continue
+ [[ -L "$project_dir" ]] && continue # skip existing symlinks
+
+ # Get the CWDs recorded in this project dir's sessions
+ while IFS= read -r cwd; do
+ [[ -z "$cwd" ]] && continue
+ [[ -d "$cwd" ]] || continue # skip if path doesn't exist (Phase 2 handles these)
+
+ # Resolve git root for this live path
+ git_common=$(git -C "$cwd" rev-parse --git-common-dir 2>/dev/null) || continue
+ [[ "$git_common" != /* ]] && git_common="$cwd/$git_common"
+ git_root="${git_common%/.git}"
+ root_slug=$(slugify "$git_root")
+ proj_name=$(basename "$project_dir")
+
+ # Record this project dir under its root slug
+ if idx=$(_find_slug_idx "$root_slug"); then
+ root_members[$idx]="${root_members[$idx]} $proj_name"
+ else
+ root_slugs+=("$root_slug")
+ root_members+=("$proj_name")
+ root_paths_arr+=("$git_root")
+ fi
+ break # one cwd is enough to identify the root
+ done < <(extract_cwds "$project_dir")
+done
+
+# Report and merge groups with more than one member
+merged_any=false
+for i in "${!root_slugs[@]}"; do
+ root_slug="${root_slugs[$i]}"
+ IFS=' ' read -ra members <<< "${root_members[$i]}"
+ [[ ${#members[@]} -le 1 ]] && continue # nothing to merge
+
+ git_root="${root_paths_arr[$i]}"
+ root_dir="$PROJECTS_DIR/$root_slug"
+ mkdir -p "$root_dir"
+
+ echo "Git root: $git_root"
+ for member in "${members[@]}"; do
+ [[ "$member" == "$root_slug" ]] && continue # skip the target itself
+ member_dir="$PROJECTS_DIR/$member"
+ meta=$(collect_metadata "$member_dir")
+ printf " -> %s (%s)\n" "$member" "${meta//|/ }"
+ merge_into "$member_dir" "$root_dir"
+ done
+ echo ""
+ merged_any=true
+done
+
+if ! $merged_any; then
+ echo "No live-path worktree sessions to merge."
+ echo ""
+fi
+
+# ── Phase 2: Deleted paths (interactive) ─────────────────────────────────────
+echo "=== Phase 2: Orphaned sessions (deleted worktree paths) ==="
+echo ""
+
+# Phase 2 uses parallel arrays for bash 3.2 compatibility (same as Phase 1)
+orphan_slugs=()
+orphan_members=()
+orphan_roots_arr=()
+unrecognised=()
+
+_find_orphan_idx() {
+ local slug="$1"
+ local i
+ for i in "${!orphan_slugs[@]}"; do
+ [[ "${orphan_slugs[$i]}" == "$slug" ]] && echo "$i" && return 0
+ done
+ return 1
+}
+
+for project_dir in "$PROJECTS_DIR"/*/; do
+ [[ -d "$project_dir" ]] || continue
+ [[ -L "$project_dir" ]] && continue # skip symlinks
+
+ proj_name=$(basename "$project_dir")
+
+ # Check if any cwd from this dir still exists — Phase 1 handled those
+ has_live_cwd=false
+ while IFS= read -r cwd; do
+ [[ -z "$cwd" ]] && continue
+ [[ -d "$cwd" ]] && { has_live_cwd=true; break; }
+ done < <(extract_cwds "$project_dir")
+ $has_live_cwd && continue
+
+ # Try to infer git root from any stored cwd
+ inferred_root=""
+ while IFS= read -r cwd; do
+ [[ -z "$cwd" ]] && continue
+ inferred_root=$(infer_git_root_for_deleted_path "$cwd") && break
+ done < <(extract_cwds "$project_dir")
+
+ if [[ -z "$inferred_root" ]]; then
+ unrecognised+=("$proj_name")
+ continue
+ fi
+
+ inferred_slug=$(slugify "$inferred_root")
+ if idx=$(_find_orphan_idx "$inferred_slug"); then
+ orphan_members[$idx]="${orphan_members[$idx]} $proj_name"
+ else
+ orphan_slugs+=("$inferred_slug")
+ orphan_members+=("$proj_name")
+ orphan_roots_arr+=("$inferred_root")
+ fi
+done
+
+if [[ ${#orphan_slugs[@]} -eq 0 && ${#unrecognised[@]} -eq 0 ]]; then
+ echo "No orphaned sessions found."
+ echo ""
+fi
+
+# ── Present each inferred group interactively ────────────────────────────────
+for i in "${!orphan_slugs[@]}"; do
+ inferred_slug="${orphan_slugs[$i]}"
+ inferred_root="${orphan_roots_arr[$i]}"
+ IFS=' ' read -ra members <<< "${orphan_members[$i]}"
+
+ display_lines=()
+ for member in "${members[@]}"; do
+ meta=$(collect_metadata "$PROJECTS_DIR/$member")
+ IFS='|' read -r sess_count size date branches <<< "$meta"
+ display_lines+=("$member | $sess_count | $size | $date | $branches")
+ done
+
+ printf "Orphaned sessions — inferred repo: %s\n" "$inferred_root"
+ $DRY_RUN && echo "(dry-run: no changes will be made)"
+
+ selected_indices=()
+ while IFS= read -r line; do
+ selected_indices+=("$line")
+ done < <(interactive_select "Merge into $inferred_slug?" "${display_lines[@]}")
+
+ if [[ ${#selected_indices[@]} -eq 0 ]]; then
+ echo " Skipped."
+ echo ""
+ continue
+ fi
+
+ target_dir="$PROJECTS_DIR/$inferred_slug"
+ mkdir -p "$target_dir"
+
+ for idx2 in "${selected_indices[@]}"; do
+ member="${members[$((idx2-1))]}"
+ member_dir="$PROJECTS_DIR/$member"
+ echo " Merging: $member"
+ merge_into "$member_dir" "$target_dir"
+ done
+ echo ""
+done
+
+# ── Unrecognisable orphans ────────────────────────────────────────────────────
+if [[ ${#unrecognised[@]} -gt 0 ]]; then
+ echo "Unrecognised orphans (cannot infer git repo):"
+ for u in "${unrecognised[@]}"; do
+ meta=$(collect_metadata "$PROJECTS_DIR/$u")
+ printf " %s (%s)\n" "$u" "${meta//|/ }"
+ done
+ echo ""
+ printf "Keep these as-is? [Y/n] "
+ keep_choice=""
+ read -r keep_choice || true
+ if [[ "$keep_choice" =~ ^[Nn] ]]; then
+ for u in "${unrecognised[@]}"; do
+ if $DRY_RUN; then
+ echo " [DRY RUN] Would delete: $u"
+ else
+ [[ "$PROJECTS_DIR/$u" == "$PROJECTS_DIR/"* ]] || { echo "ERROR: refusing to remove $u" >&2; continue; }
+ rm -rf "${PROJECTS_DIR:?}/$u"
+ echo " Deleted: $u"
+ fi
+ done
+ else
+ echo " Kept."
+ fi
+ echo ""
+fi
+
+echo "Done."