hocketdocs

Git mutation surface

ADR-0007 makes hocket observe-only, then carves out a short list of guarded, user-initiated local writes through its amendments. The records live in the ADR; this page is the operational map — every write hocket can perform, the guards on it, and how the destructive ones compose with dirty trees, live agents, remotes, and recoverability. All writes go through core/git/run.ts (gitWrite), argv arrays, never a shell; a failure surfaces as a status-bar notice and changes nothing.

WriteCommandGestureGuards
Create worktree + branchworktree add -⁠bmenus/palette, typed nameagent path convention, user-initiated only
Mount existing branchworktree addbranch menu
Create branchbranch <name> <from>menus/palette
Rename branchbranch -⁠mbranch menuunmounted, non-trunk
Record branch baseconfig -⁠-⁠local branch.<n>.hocket-⁠baseautomatic rider on create gestureskey prefix-locked in code; metadata only, never refs/objects; failure is silent (inference covers)
Retarget children's basesame command, per childautomatic rider on hocket renamesame key lock; external renames instead degrade to trunk at the next scan
Delete branch (soft)branch -⁠dbranch menu, confirmunmounted, non-trunk, merged into trunk; -⁠d re-proves it
Remove worktreeworktree removeworktree menu, confirmnon-main, clean, fresh scan, not unmerged-detached; live sessions and observed processes offer end-and-remove (red confirm); branch preserved
Delete worktree + branchworktree remove then branch -⁠d, escalating to -⁠D only when the confirm carried the unmerged warningworktree menu, confirmevery Remove-worktree guard, then the branch rung: non-trunk (menu-gated), merged states soft stakes and -⁠d re-proves; unmerged states the hard-delete stakes up front
Switch root to trunkworktree remove (a mounted trunk's worktree) then switch <trunk>root checkout menuroot only, off trunk (or detached); live sessions in trunk's worktree render the item inert; never forced — git refuses a dirty/untracked trunk worktree (remove) and conflicting root changes (switch); no confirm, nothing can be lost
Auto-prunethe two aboveopt-in, automaticmerged + clean + idle only, re-checked at prune time
Hard delete branchbranch -⁠Dgraph ref chip, red confirmunmounted, non-trunk
Drop archived bookmarkupdate-⁠ref -⁠dgraph ref chip, red confirmprefix-locked to refs/archived-⁠worktrees/ in code

Remote refs, merge/rebase, commits, history rewriting, and anything over the network remain outside the boundary entirely.

Lineage metadata

The two config writes above are the lineage rider (ADR-0017, ADR-0007's lineage amendment): one inert key per branch naming the branch it was forked from. The key's lifecycle rides git's own section handling — branch -m migrates it, branch -d/-D removes it — so external renames and deletes need no hocket-side GC. Deleting a base re-parents its children to trunk at the next observation (a read-side collapse, no write). Auto-prune performs no lineage writes. Branches without the key fall back to reflog inference, then trunk-default, read-only.

The deletion ladder

Deletion is ordered so that each rung's guard makes the next rung safe:

              ref to delete
                    │
                    ▼
         mounted in a worktree? ── no ──────╮
                    │ yes                   │
                    ▼                       │
╭ remove worktree first ───────────────╮    │
│ clean + fresh scan; live sessions    │    │
│ and observed processes offer         │    │
│ end-and-remove (red confirm);        │    │
│ Delete worktree + branch walks       │    │
│ both rungs on one confirm            │    │
╰──────────────────────────────────────╯    │
                    │                       │
                    ▼                       │
              now unmounted ◀───────────────╯
                    │
                    ▼
           merged into trunk? ── no ───────────────────╮
                    │ yes                              │
                    ▼                                  ▼
    ╭ soft delete · branch -d ─────╮   ╭ hard delete · branch -D ─────╮
    │ sidebar menu or auto-prune   │   │ graph ref chip · red confirm │
    ╰──────────────────────────────╯   ╰──────────────────────────────╯
  • The paired delete rides the ladder, it doesn't skip it. "Delete worktree + branch" is one gesture over the same two rungs: the removal rung keeps every guard (and the end-and-remove path for occupants), then the branch rung runs with the merged question answered in the confirm the user already read — soft stakes and -d when the work landed, the hard-delete warning up front when it didn't. Even behind that warning -d is tried first, so a branch that never committed is dropped without force and -D fires only for a tip that would actually go unreachable. If the branch delete refuses (say the tip advanced past a stale merged read), the branch survives and is revealed, exactly as if the preserving remove had run.
  • Dirty trees can never reach hard delete. Only unmounted branches offer it from the graph, and an unmounted branch has no working tree to be dirty. A mounted branch's routes to deletion run through worktree removal — beforehand, or in the same paired gesture — and that step owns the working-state proof: clean tree plus a fresh process scan, because "not observed" is not "idle" (an unopened Processes pane has no sample). A failed scan blocks removal. Live sessions and observed processes are soft rungs: the red end-and-remove confirm lists sessions and unrelated processes, closes the confirmed sessions, then uses SIGTERM, a short grace, and SIGKILL for survivors that still match the confirmed command identity (ADR-0012's pid-reuse guard). The removal that follows is still the non-forced worktree remove, so a shutdown hook that dirties the tree makes git itself refuse. Uncommitted work is therefore protected one rung before any ref drops.
  • Working agents are worktree-shaped. Sessions and processes attach to worktrees, never to unmounted branches, so the rows that carry agent glyphs (worktree ghost rows) never offer hard delete — their menu is the worktree menu. An agent busy in one worktree does not block deleting an unrelated parked branch, and shouldn't.
  • The root checkout never enters the ladder. It is the repo, not a removable anchor: its menu renders Remove inert with the reason ("root checkout") and offers the one write that goes home instead — switch <trunk>, non-forced, no confirm, because switch refuses rather than discards (git carries non-conflicting changes and rejects the rest). A trunk mounted in a linked worktree comes home first via the same non-forced worktree remove — deliberately without the ladder's confirm, because its object is a clean checkout of trunk's own tip immediately re-materialized at the root; only live sessions there hold the gesture inert. If the remove lands and the switch refuses, trunk parks unmounted (pinned second) with the plain switch still offered.
  • The race window is the confirm modal. The menu item names a ref, not a sha; branch -D drops whatever the ref points at when confirmed. If an agent advances the branch between right-click and return, the newer tip is what becomes unreachable — same contract as the sidebar's soft delete, where -d's own merged re-check narrows the window further. Git is re-read after every write, so the UI never keeps showing a deleted ref.

Remotes

  • Hocket still never touches refs/remotes or the network: no fetch, no push, no remote branch deletion. Hard delete does not change this.
  • branch -D drops the local ref (and its branch.<name> config). A pushed branch survives at origin/<name> and is recoverable by recreating the branch from it — hard-deleting a pushed branch discards nothing but your local pointer.
  • "Leaves the graph" is a projection statement, not a repo statement. The graph walks from local heads, worktree HEADs, and archived bookmarks only; remote refs are neither drawn nor walked. Commits reachable only from origin/<name> disappear from the graph while remaining safely in the repository.
  • Hocket tracks no upstream state for unmounted branches (ahead/behind comes from status porcelain, per worktree), so the confirm cannot yet say "also exists on origin". If that guard ever earns its keep, it belongs on BranchState via for-each-ref %(upstream), not in the confirm text.

Recoverability

  • Soft delete is provably lossless-d verifies merged ancestry inside git; the commits remain in trunk history.
  • Hard delete drops a ref, never objects. The commits stay in the object store and remain reachable through any other ref or worktree HEAD that holds them (a detached worktree parked on the tip keeps it alive — and in the graph). Truly unreachable commits survive until git gc prunes them (~two weeks by default); until then they are recoverable by sha (git fsck --lost-found, the creating agent's scrollback, or the remote). The branch's own reflog is deleted with the branch, so recovery is by sha, not by name.
  • Removing a worktree deletes an anchor, never commits. A detached checkout's tip is reachable only through the worktree's own HEAD, which lives in the main repo's .git/worktrees/<name>/ admin directory; worktree remove deletes that directory — HEAD and its reflog included — so an unmerged detached tip goes unreachable and leaves the graph in the same stroke. That is exactly the loss the "not unmerged-detached" guard refuses. The commits themselves follow the hard-delete rules above: they sit in the object store until git gc prunes, recoverable by sha (git fsck --unreachable --no-reflogs, then git branch <rescue> <sha>) — never by reflog, which died with the worktree. Origin never notices any of this: fetch and push negotiate refs only, so unreachable local objects are invisible to the remote and no sync state changes.
  • Archived bookmarks are pure pointers. Hocket never creates refs/archived-worktrees/*; dropping one deletes only the bookmark, and its commits follow the same reachability rules as any hard delete.

After every write

Every mutation ends in the same refresh: the observer re-reads porcelain, the graph source re-probes (the ref hash moved, so the layout rebuilds), the graph cursor clamps to the shrunken grid, and the sidebar reselects the surviving branch or worktree. Nothing in the UI trusts its own write — git is re-read as the only truth (ADR-0011 refresh doctrine).