feat(git): a local surface to switch to an existing branch #867

Open
opened 2026-10-21 21:05:00 +00:00 by hexajon · 1 comment
Owner

The gap

madtea wraps the branch moves that start and finish work. madt_branch_create creates and checks out a new branch. madt_finish merges and returns to the default branch. madt_pull, madt_push, madt_fetch, madt_branch_delete, madt_commit, and madt_worktrees cover the rest. No surface switches to a branch that already exists: back to the default branch, or across to another existing branch.

A caller that drives git only through the madt_* tools hits this the moment it changes course. It runs madt_branch_create, then wants to leave that branch and return to the default branch without finishing it. The only way back is raw git switch or git checkout, the exact fallback the tool set exists to remove. The Tier-1 goal is a clean install that drives the forge with no raw-git fallback (ADR 0001), and this one move breaks that goal.

This is the shape of #571: every git operation on this path has a madt_* surface except one, and the missing one falls out of the set at the moment it is needed.

What is missing

  • A switch of HEAD to an existing local branch, the default branch included.
  • Not a create. madt_branch_create already creates and checks out a new branch. This surface checks out a ref that already exists, and never creates one.

Design

  • Name. The CLI verb is switch and the MCP tool is madt_switch. git's switch is the branch-only verb, so it maps one to one to this capability and avoids checkout's overloaded file form. git still marks switch experimental, but only on its newer flags; the plain switch <branch> behaviour has been stable since git 2.23. ADR 0016 and ADR 0030 anchor the name and shape to git, and the MCP surface keeps its named-parameter form (ADR 0030).
  • Dirty-tree rule. The surface mirrors git. It carries non-conflicting uncommitted changes onto the target branch, and refuses, with git's own message, only when the switch would overwrite a change. No work is discarded either way. This matches what madt_branch_create already does in its own repo.
  • Foreign primary. A switch moves the primary checkout's HEAD, the shared-state clobber ADR 0019 already refuses for madt_branch_create and madt_pull. A foreign-primary switch refuses the same way, and names the worktree flow.
  • One service seam under both surfaces (ADR 0014), as madt_branch_create already has.

Acceptance criteria

  • madtea switch <branch> and madt_switch move HEAD to an existing local branch, the default branch included, and report the branch and the new short HEAD sha.
  • A name that no local branch holds is an error that points at madt_branch_create; the surface never creates a branch.
  • Non-conflicting uncommitted changes carry onto the target branch; a switch that would overwrite a change refuses with git's own message. A test covers both.
  • A foreign primary checkout is refused, consistent with ADR 0019, and the refusal names the worktree flow.
  • Both the CLI and MCP carry the surface, held by the parity test in internal/parity/.
## The gap madtea wraps the branch moves that start and finish work. `madt_branch_create` creates and checks out a new branch. `madt_finish` merges and returns to the default branch. `madt_pull`, `madt_push`, `madt_fetch`, `madt_branch_delete`, `madt_commit`, and `madt_worktrees` cover the rest. No surface switches to a branch that already exists: back to the default branch, or across to another existing branch. A caller that drives git only through the madt_* tools hits this the moment it changes course. It runs `madt_branch_create`, then wants to leave that branch and return to the default branch without finishing it. The only way back is raw `git switch` or `git checkout`, the exact fallback the tool set exists to remove. The Tier-1 goal is a clean install that drives the forge with no raw-git fallback (ADR 0001), and this one move breaks that goal. This is the shape of #571: every git operation on this path has a madt_* surface except one, and the missing one falls out of the set at the moment it is needed. ## What is missing - A switch of HEAD to an existing local branch, the default branch included. - Not a create. `madt_branch_create` already creates and checks out a new branch. This surface checks out a ref that already exists, and never creates one. ## Design - Name. The CLI verb is `switch` and the MCP tool is `madt_switch`. git's `switch` is the branch-only verb, so it maps one to one to this capability and avoids `checkout`'s overloaded file form. git still marks `switch` experimental, but only on its newer flags; the plain `switch <branch>` behaviour has been stable since git 2.23. ADR 0016 and ADR 0030 anchor the name and shape to git, and the MCP surface keeps its named-parameter form (ADR 0030). - Dirty-tree rule. The surface mirrors git. It carries non-conflicting uncommitted changes onto the target branch, and refuses, with git's own message, only when the switch would overwrite a change. No work is discarded either way. This matches what `madt_branch_create` already does in its own repo. - Foreign primary. A switch moves the primary checkout's HEAD, the shared-state clobber ADR 0019 already refuses for `madt_branch_create` and `madt_pull`. A foreign-primary switch refuses the same way, and names the worktree flow. - One service seam under both surfaces (ADR 0014), as `madt_branch_create` already has. ## Acceptance criteria - [ ] `madtea switch <branch>` and `madt_switch` move HEAD to an existing local branch, the default branch included, and report the branch and the new short HEAD sha. - [ ] A name that no local branch holds is an error that points at `madt_branch_create`; the surface never creates a branch. - [ ] Non-conflicting uncommitted changes carry onto the target branch; a switch that would overwrite a change refuses with git's own message. A test covers both. - [ ] A foreign primary checkout is refused, consistent with ADR 0019, and the refusal names the worktree flow. - [ ] Both the CLI and MCP carry the surface, held by the parity test in `internal/parity/`.
Author
Owner

Decisions on the open points

I settled the points that the design above left open:

  1. Remote-only name. A name that exists only as a remote-tracking ref (for example origin/feat-b, with no local feat-b) is refused. madtea runs git switch --no-guess, so git never creates a local tracking branch. The error points at madt_branch_create with base=origin/feat-b.
  2. Foreign linked worktree. A linked worktree of a foreign repo with modified or staged tracked files is refused, the same as madt_branch_create. Untracked files do not count. In my own repo the dirty-tree rule mirrors git, as stated above.
  3. Tag or sha. The surface takes a local branch only. A tag or a commit sha is refused. There is no detach option.
  4. Current branch. A switch to the branch that is already checked out succeeds, and the result says that HEAD did not move.
  5. No create flag. The surface never creates a branch, as stated above, so switch has no -c.
  6. Closure marker. A successful switch ends with the closure marker, the same as madt_branch_create. madt_switch joins the completion set of the closure drift test.
  7. Hook. This issue makes no change to the Claude Code hooks. A separate issue covers a steer for a plain same-repo git switch.
  8. ADR 0019. This branch edits ADR 0019 in place, so its list of refused tools on a foreign primary names madt_switch.
## Decisions on the open points I settled the points that the design above left open: 1. Remote-only name. A name that exists only as a remote-tracking ref (for example `origin/feat-b`, with no local `feat-b`) is refused. madtea runs `git switch --no-guess`, so git never creates a local tracking branch. The error points at `madt_branch_create` with `base=origin/feat-b`. 2. Foreign linked worktree. A linked worktree of a foreign repo with modified or staged tracked files is refused, the same as `madt_branch_create`. Untracked files do not count. In my own repo the dirty-tree rule mirrors git, as stated above. 3. Tag or sha. The surface takes a local branch only. A tag or a commit sha is refused. There is no detach option. 4. Current branch. A switch to the branch that is already checked out succeeds, and the result says that HEAD did not move. 5. No create flag. The surface never creates a branch, as stated above, so `switch` has no `-c`. 6. Closure marker. A successful switch ends with the closure marker, the same as `madt_branch_create`. `madt_switch` joins the completion set of the closure drift test. 7. Hook. This issue makes no change to the Claude Code hooks. A separate issue covers a steer for a plain same-repo `git switch`. 8. ADR 0019. This branch edits ADR 0019 in place, so its list of refused tools on a foreign primary names `madt_switch`.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
sixfold-space/madtea#867
No description provided.