WIP: Worktrees from the phone: three declared capabilities - #135
Open
broven wants to merge 7 commits into
Open
Conversation
Herdr can create a Git worktree and open it as a space; Collie had no route to it, so branching off from the phone meant walking back to the desk (AltanS#133). The port learns four verbs — list, create, open, remove — because the ACT is the multiplexer's even though the noun is Git's: each one ends in a space appearing, moving or going away, and the record tying a checkout to the space showing it is what a multiplexer may or may not keep. Herdr keeps it and declares all four; tmux and zellij decline them with their reasons, since a checkout they made could not be listed or removed again. ADR 0032 has the argument, including why `git worktree add` + `createSpace` is the wrong cheap route. Removal is addressed by SPACE, never by path — that is Herdr's shape, not a preference, and `MuxWorktree.openSpaceId` states it in the type so a caller knows before it asks. `focus` is never sent: the phone navigates itself and the operator's own screen stays put. Every Herdr cell in the matrix cites a first-hand probe of herdr 0.8.2 on 2026-08-28 (isolated session, throwaway repo), including the one that matters most: `worktree.create` is NOT atomic — a failed open leaves the branch on disk with nothing showing it, and the recovery is to open it, never to create it again. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four routes behind the four capabilities: list the repo's worktrees, create
one on a new branch, open one that already exists, remove the one a space is
showing.
The space IS the repo context. `MuxSpace` learns `repoRoot` — a declared fact,
like `spaces` capacity, not a capability — and Herdr fills it from the block it
already puts on every workspace record. So no route takes a repo path, nothing
walks the filesystem looking for `.git`, and a space outside a repo simply has
no key (absence is the fail-closed answer the phone reads as "no worktree
rows"). The snapshot gains the field only where there is one, so an ETag moves
for a space in a repo and for nothing else.
Removal names its target in the BODY while the path segment stays the asking
space: the sheet lists siblings, so those are two different spaces. The asking
space must itself be in a repo, which keeps the verb from being a general
"close any space" door reachable by naming one.
Three refusals get their own code because the phone ACTS on them — a dirty
checkout arms the discarding tap, a busy multiplexer means retry, an ambiguous
branch means type a better one. A fourth is the half-done create: the checkout
exists and only the opening failed, so it is `worktree.created_not_opened` and
the recovery is to open it, never to create it again. Everything else shares one
code per verb and carries the multiplexer's own sentence in `{reason}`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The phone side of AltanS#133: branch a new worktree, open one that exists, remove the one a space is showing — reachable from the space you are already in. Shown only where BOTH answers are yes: the multiplexer declares `listWorktrees`, and this space reports a `repoRoot`. Both ride on data the route already has, so a space outside a repo shows nothing and costs no extra call. Three behaviours are load-bearing, and each is a probed reality rather than a preference: • Remove is absent for a checkout no space is showing — removal is addressed by space, so there would be nothing to name. The row says why instead of offering a tap that must fail. • The discarding confirmation appears only AFTER the multiplexer has refused for being dirty, and only on the checkout that refused. `force` is never sent on a first attempt: two taps mean "remove it", and a third — worded as discarding the work — is a separate decision the operator has been told the cost of. • A half-created worktree offers "open", never "create again", because the branch is already on disk and a second create refuses. Opening navigates the PHONE. `already_open` is treated as an answer, not a failure — either way the pane is where to go — and the desktop's own screen is never moved. Six locales, because the dictionary is total by type. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… which kind Two things the flat list could not say, both from testing the sheet on a real herd. **The spaces list nests.** A worktree sits one level under the space showing its repo, which is the relationship the list was flattening away — four of six spaces here are two repos and their worktrees, and nothing said so. `MuxSpace` gains `isWorktree` beside `repoRoot` (the pair travels together and Herdr answers both on the workspace record), so the nesting needs no extra call. Three rules, each answering a case the flat list never had: a group takes the position of its MOST RECENT member, so a worktree used a minute ago does not get buried under a checkout last touched a week ago; a worktree whose repo is not open stays flat, because indenting under nothing reads as a rendering fault; and filtering flattens, for the same reason. **One create entry point, two kinds.** The `+` on the spaces list now opens a sheet with a tab: a plain space, or a worktree. The worktree side has to ask which repo — from the list there is no current space to carry one — so it offers the repos that are open, defaulting to the most recent. The tab strip appears only where there is a choice: no capability, or nothing open in a repo, and it is a plain new-space sheet exactly as before. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
broven
marked this pull request as draft
August 28, 2026 06:53
…their own
The Worktrees button and its sheet are gone. Testing them on a real herd, the
button read as a panel about a different concept — and it was, which is why
nothing about it said what it was for.
Every verb moves to where this repo already puts that verb:
• list — the spaces list itself, now that worktrees nest under their repo.
A worktree IS a space; a second list of them was the same thing
under two names.
• open — tapping the row, like any space.
• create — the `+` sheet's Worktree tab.
• remove — LONG-PRESS the space, then a two-tap destructive row: exactly the
shape a tab's close and a pane's close already take.
The one worktree kind with no other route — one that exists on disk and is
open as nothing — gets a short "or open one that already exists" list inside
the Worktree tab, read once per repo choice rather than on any poll.
WORTH A REVIEWER'S EYE: Collie has no `closeSpace` capability and this does not
add one. The space actions sheet offers removal only where the thing removed is
a git worktree; the space going away is Herdr's own consequence of
`worktree.remove`, not a power Collie granted itself. A space that is not a
worktree says so, in those words.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Removal is out: the port verb, the route, the codes, the sheet and its strings. What is left is list, create and open. The reason is not that it was hard, it is that it was reaching past what Collie has decided. There is no `closeSpace` capability here, deliberately — and Herdr's `worktree.remove` closes the space along with the checkout, so shipping it would have handed Collie its first space-destroying verb through a side door nobody argued for. A checkout that has outlived its use is still cleaned up at the desk, which is where every other space-ending decision is made today. `MuxWorktree.openSpaceId` stays, because it is what tells the phone which checkouts are already spaces — the nesting and the "or open one that already exists" list are both built on it. Removal can be added later against that same field, as its own argument rather than as a rider on this one. ADR 0032 is reframed to match: the bookkeeping argument for asking the multiplexer instead of running Git now stands on listing and re-opening, which is where it always did its real work; removal appears as the sharpest illustration of the point rather than as one of the verbs shipped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The states playground's rule is that every state Collie can reach is on that page, and these two were not: a spaces list nesting a worktree under its repo, and the new-space sheet's worktree tab. Both mount the real components with real props — the nesting card passes an empty herd deliberately, because with agents attached every row also carries its triage tint and a wall of red says nothing about shape. `spacesWithWorktrees` carries the fields the multiplexer actually reports (`repoRoot`, `isWorktree`), so the card is the real shape rather than a staged one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
broven
marked this pull request as ready for review
August 28, 2026 07:49
Owner
|
Just a heads up, this PR is against the v1 branch (not main) and I dont have a clear release path for v1 yet. Not sure if that was intended, but it definitely saves me some time as I won't have to port the feature back into v1. There is a path to install the beta which are outlined in the v1's readme. Let me know if you have troubles setting it up. If this was meant for the current main branch, please close and resubmit the PR. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #133.
Branch off and put an agent on it from the phone. Three capabilities —
listWorktrees,createWorktree,openWorktree— behind three routes, surfacing in two places.A worktree nests under the space showing its repo
MuxSpacegainsrepoRootandisWorktree— declared facts, likespacescapacity, never capabilities — and Herdr answers both on the workspace record it already sends, so the nesting costs no extra call and needs nothing switched on.Three rules, each answering a case the flat list never had:
A space outside any repo is the row it always was.
+asks which kindThe worktree side asks which repo, because a sheet opened from the list has no current space to take one from. It offers the repos that are open, defaulting to the most recently used. Under the branch field sits "or open one that already exists" — the worktrees of the chosen repo that no space is showing, read once per repo choice. Those are the only checkouts the phone has no other route to; everything else is already a space in the list behind the sheet.
The tab strip renders only where there is a choice. No capability, or nothing open in a repo, and this is the plain new-space sheet, unchanged.
Opening navigates the phone.
focusis never sent, so the operator's own screen stays where they left it — moving that issetFocus, which is its own capability for exactly this reason.Both states are cards in the states playground, each with its "reach it for real" line.
Why the port, and not
git worktree add+createSpaceThe cheaper route is the obvious one, and ADR 0032 is there for when it is proposed again.
Herdr does not merely run Git; it keeps a record —
open_workspace_idper checkout, aworktreeblock on every workspace. That record is the feature: it is how the phone knows which checkouts exist, which of them a space already shows, and which space nests under which repo. A checkout made behind Herdr's back has none of it, and Collie could only list it by doing its own Git work and then reconciling two answers about one directory.Herdr declares all three. tmux and zellij decline them with their reasons: tmux could shell out to Git, but it keeps no mapping from a checkout to the session showing it, so what it made could not be found again. Declining is the honest answer until an adapter keeps that record itself and a probe proves it.
Probed, not assumed
Every Herdr cell in
MUX_CONTRACT.mdcites a first-hand probe of herdr 0.8.2 (protocol 20), 2026-08-28, run against an isolatedherdr --sessionover a throwaway repo — never a live pane. Two findings shaped the code:worktree.createis not atomic. In a session with no window server the checkout was created and the open failed withworktree_open_failed: the branch exists, nothing shows it, and creating again answersworktree_create_failedbecause the path is taken. That case gets its own code —worktree.created_not_opened— and the UI offers open, never create again.worktree.openis idempotent, answeringalready_openwith the space showing it. Treated as an answer rather than a refusal: either way the pane below is where to go.Refusals reach the phone as catalogued codes. Three change what it does — a busy multiplexer means retry, an ambiguous branch means type a better one, a space outside a repo shows nothing at all — and the rest carry the multiplexer's own sentence in
{reason}.Notes
- Worktrees from the phone: create or open a Git worktree from the spaces list, and see it nested under its repo. (<hash>)bun run lintclean,bun testand the web suite green.🤖 Generated with Claude Code