Merge pull request #71665 from NousResearch/bb/deleted-worktree-fold

fix(project-tree): absorb deleted-worktree sessions into the parent home checkout
This commit is contained in:
brooklyn! 2026-07-25 20:42:43 -05:00 committed by GitHub
commit 1f6427b75e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 51 additions and 0 deletions

View file

@ -466,6 +466,26 @@ def test_broad_default_non_git_cwd_stays_unscoped():
assert detached["id"] not in tree["scoped_session_ids"]
def test_deleted_sibling_worktree_folds_into_parent_home_checkout():
# A deleted <repo>-<suffix> worktree leaves its session with an unresolvable
# cwd and no persisted root. It joins the parent's trunk lane — no dead-path
# lane, no phantom project.
resolve = _resolver({"/www/hermes-agent": ("/www/hermes-agent", "/www/hermes-agent")})
sessions = [
_session("/www/hermes-agent", branch="main"),
_session("/www/hermes-agent-session-links"),
]
tree = pt.build_tree([], sessions, [], resolve, hydrate=True)
project = tree["projects"][0]
assert [p["id"] for p in tree["projects"]] == ["/www/hermes-agent"]
assert _lane_ids(project) == ["/www/hermes-agent::branch::main"]
main = project["repos"][0]["groups"][0]
assert main["isMain"] and main["path"] == "/www/hermes-agent"
assert len(main["sessions"]) == 2
def test_colliding_repo_basenames_disambiguate_labels():
resolve = _resolver(
{

View file

@ -41,6 +41,11 @@ _KANBAN_DIR_RE = re.compile(r"^(.*[/\\]\.worktrees)[/\\]t_[0-9a-f]+[/\\]?$")
_TRUNK_BRANCHES = {"main", "master", "trunk", "develop"}
DEFAULT_BRANCH_LABEL = "main"
# How many sibling candidates to try when recovering a deleted worktree's parent
# repo (see ``_probe_sibling_worktree``). Each miss costs a git probe, so keep it
# tight — real suffixes are one or two segments.
_MAX_SIBLING_PROBES = 4
def _branch_lane_id(repo_root: str, branch: str = "") -> str:
"""The one definition of a main-checkout lane id (must match the desktop)."""
@ -146,6 +151,24 @@ def _placement(
}
def _probe_sibling_worktree(cwd: str, resolve: Resolve) -> str:
"""The parent repo root of a deleted ``<repo>-<suffix>`` worktree, else ``""``.
A deleted worktree dir can't be probed, so walk back up its name — trimming
one ``-<segment>`` at a time and return the first sibling that resolves.
Probes are bounded and served from the shared git-probe cache.
"""
parts = base_name(cwd).split("-")
floor = max(0, len(parts) - 1 - _MAX_SIBLING_PROBES)
for i in range(len(parts) - 1, floor, -1):
info = resolve(_with_base_name(cwd, "-".join(parts[:i])))
if info and info.get("repo_root"):
return (info["repo_root"] or "").strip()
return ""
def _place_by_heuristic(path: str) -> Optional[dict]:
"""Path-only fallback when there is no git probe and no persisted root."""
base = base_name(path)
@ -195,6 +218,14 @@ def _place(cwd: str, branch: str, resolve: Optional[Resolve], persisted_root: st
b = (branch or "").strip() or DEFAULT_BRANCH_LABEL
return _placement(persisted_root, _branch_lane_id(persisted_root, b), b, persisted_root, True, False)
# Unresolvable cwd: a deleted ``<repo>-<suffix>`` worktree still belongs to
# its parent. It has no checkout to return to, so absorb it into the trunk
# lane rather than stranding a dead-path lane in the project forever.
sibling_root = _probe_sibling_worktree(cwd, resolve) if resolve else ""
if sibling_root:
b = (branch or "").strip() or DEFAULT_BRANCH_LABEL
return _placement(sibling_root, _branch_lane_id(sibling_root, b), b, sibling_root, True, False)
return _place_by_heuristic(cwd)