0054 — Worktree-bound sessions by name convention
0054 — Worktree-bound sessions by name convention
TL;DR. phux worktree composes git worktree with the existing session
verbs. The binding between a checkout and a phux session is a pure function of
the worktree path — a derived session name — not stored state. The server
learns nothing about git, and no wire message changes.
Status: Accepted Date: 2026-07-25
Context
Running several coding agents at once means running several checkouts at once.
Today that is two manual steps every time: git worktree add, then phux new -s something -c that-path, with the operator remembering which session belongs
to which checkout. Removing a checkout is worse — the session outlives the
directory, and its panes sit in a path git has deleted underneath them.
Every multiplexer that has solved this has done it by teaching its daemon about git. That is the option to reject deliberately, not by omission.
Decision
phux worktree is a client-side convenience layer over git worktree plus the
already-shipped new / ls / kill verbs. It adds four actions — list,
new, open, remove — and no server state, no protocol field, and no
persistent mapping file.
A worktree binds to exactly one session name, derived from the worktree’s
directory basename by a total, deterministic function: characters outside
[A-Za-z0-9._-] collapse to -, runs of - collapse to one, leading
selector-reserved characters (@, #, =, .) are stripped, and the empty
or .-only result falls back to worktree. The derivation is pure, so any
client — the CLI, the TUI, an agent — computes the same name from the same path
without consulting anything.
list reports each worktree with its derived name and whether a session by
that name currently exists. new runs git worktree add and then creates the
session rooted in the new checkout. open creates the session if absent and is
otherwise a no-op that prints the name. remove kills the bound session first,
then runs git worktree remove, and refuses a dirty checkout unless --force
is passed.
Rationale
The derived name is correct at every instant because it is never stored. A
mapping table would need invalidation on every event phux cannot observe: git
deleting a worktree, an operator mv-ing the directory, a session renamed
through phux rename. Deriving on demand has no stale state to reconcile
because it has no state.
It also keeps the layering honest. A worktree is a VCS concept; a Terminal is
the phux unit (ADR-0009). Teaching phux-server about git would put a
filesystem-watching, git-parsing subsystem inside the one process that must
stay alive across every client crash. The composition lives where composition
belongs — in the CLI, next to the other verbs it composes.
Killing the session before removing the directory is ordered that way because the reverse order is the failure users actually hit: git refuses to remove a worktree whose files are held open, and a shell with that cwd holds it open.
Tradeoffs
Two worktrees whose basenames collide derive the same session name. new
detects the collision and fails rather than adopting the other checkout’s
session; the operator passes -s NAME to disambiguate. This is accepted
because the alternative — auto-suffixing — produces names nobody can predict,
which defeats the point of a derivable binding.
A session created by hand in a worktree path, under a different name, is not
recognized as bound. list will show the worktree as unbound even though a
session is sitting in it. Fixing this needs pane cwd in the session snapshot,
which is a protocol addition this ADR deliberately does not make.
remove shells out to git for the dirty check rather than reimplementing it,
so its refusal semantics track whatever the installed git does.
Alternatives considered
Server-side worktree registry. A WORKTREE_* message family with the
daemon owning the mapping. Rejected: it makes the server depend on git being
installed and on a directory the server does not control, for a feature that
composes fine without it.
Sidecar mapping file ($XDG_STATE_HOME/phux/worktrees.json). Rejected for
the staleness argument above — every path that mutates a worktree outside phux
silently corrupts it.
Session cwd in the snapshot, matched against git worktree list. This is
the correct long-term answer and is strictly better than name derivation, but
it is a wire change with its own compatibility surface. Filed rather than
bundled; the derived name is forward-compatible with it because a cwd-matched
binding can only ever be a superset of the name-matched one.