Skip to content

Contributing & worktrees

Want to hack on nebelhaus itself — the rice, the launcher, or the palette? This page covers how the family is developed. (To just use nebelhaus, you never need any of this — see Keeping in sync.)

The workshop repo is a parent directory that holds every family repo checked out side by side, plus a bench dev CLI (a different tool from the end-user haus). Clone it and pull the family:

Terminal window
git clone https://github.com/nebelhaus/workshop.git
cd workshop
./bench clone

You end up with nebelung/, pounce/, nebelhaus/, and friends as independent git repos, with the bench script sitting above them.

Each change belongs to exactly one repo. Start in the right one:

ChangeRepo
colours / palette / how a tool is themednebelung
the launcher app or a generic commandpounce
the Messages client (trill)trill
the notch file shelf (perch)perch
macOS defaults, tiling, bar, shell, Touch IDnebelhaus
your own machine’s apps / identity / secrets~/.config/nix

The important trick is that you never have to push to see a change. The workshop bench try builds your real machine config against the local checkouts — uncommitted edits and all — using Nix --override-input:

Terminal window
# edit anything in nebelung/, pounce/, nebelhaus/…
./bench try # does it build? (nothing pushed, nothing activated)
./bench try switch # run it on this Mac (still nothing pushed)

When you’re happy, commit in the repos you touched and let bench ship ripple the change downstream — pushing each repo and bumping every flake.lock in dependency order, so a colour change in nebelung reaches your Mac without you hand-walking the pin chain:

Terminal window
./bench ship

./bench status shows every repo’s git state and every stale lock edge at a glance.

nebelhaus is built to be hacked on by several Claude Code agents at once. Each agent runs in its own git worktree — an isolated checkout on a worktree-<name> branch, living outside the repo — so agents never yank the branch out from under each other. That whole mechanism (spawning with Super c, never losing work when a pane closes, resuming a parked session, the statusline HUD) is the rice’s wt tool, and it works for any repo. Two things matter specifically when the repo is a family one:

  • bench try is worktree-aware. Run it from inside a worktree and that repo’s flake override points at your branch instead of the main checkout — so an agent can prove its branch builds before anyone merges it. (bench try switch refuses from a worktree: only the main checkouts get activated.)
  • Land work through a PR — never a push or local merge into main. Parallel agents pushing straight to main clobber each other; a PR is conflict-detected and atomic. Push the branch, open a PR with a What / Why / Verify / Watch-out body, and merge it from GitHub. bench status lists every agent worktree and unmerged worktree-* branch so nothing rots silently.

Working from the workshop on a task that belongs to a sub-repo? Resolve the workshop’s main checkout from Git, then make the child worktree — this works no matter where you cloned the workshop:

Terminal window
workshop_root="$(dirname "$(git rev-parse --path-format=absolute --git-common-dir)")"
cd "$(wt child "$workshop_root/nebelung")" # replace nebelung with the repo you need

That registers the child so its PR shows in the statusline, which a raw git worktree add would not.

Feel-testing the whole PR queue (try-batch)

Section titled “Feel-testing the whole PR queue (try-batch)”

Activating a Mac is serial — one darwin-rebuild switch is one machine state — so with a stack of PRs waiting you can only feel-test one at a time. The tempting shortcut is to merge them all to main first, then rebuild and tick them off — but now unverified code is on main before you’ve felt it.

bench try-batch inverts that. For each repo it builds a throwaway integration tree (the committed main plus every open PR merged in), overrides the flake at those trees, and builds — or activates — the whole queue in one rebuild, main untouched:

Terminal window
./bench try-batch # build every open PR together; prints a tick-off checklist
./bench try-batch switch # …and activate the combined tree on this Mac

Verify each PR (its body carries the Verify steps), then merge only the ones that pass and leave the rest open. This is test-then-merge, not merge-then-test.

One rice change escapes the ripple entirely. A zellij edit — a keybind, a theme colour, a freshly-built plugin .wasm — doesn’t need bench try switch at all: that restarts the main zellij session and nukes every open tab. The rice ships zscratch for exactly this. It renders your candidate over a copy of your live ~/.config/zellij into a temp config dir and boots a throwaway session in its own Ghostty window, so your working multiplexer is untouched:

Terminal window
zscratch --config config.kdl # try a candidate config.kdl
zscratch --layout my-layout.kdl # …or a layout
zscratch --theme nebelung.kdl # …or a theme
zscratch --plugin tab-bar=tab-bar.wasm # …or swap a freshly-built plugin
zscratch clean # kill the scratch session + temp dir

Feel it there; the real bench try switch happens once, at the end, already knowing it works. After that activation, press Super R (or run zreload) to gracefully quit/reopen Ghostty and load the installed zellij config and plugin wasm through a fresh server without rebuilding the workspace by hand. The destructive half runs in a launchd agent outside the terminal, so it survives both processes going away. Tabs, panes and working directories return; live Claude Code panes resume their exact conversations. Pane identity comes from a generation-scoped manifest rather than zellij’s foreground-command label (which can temporarily be an LSP or tool); an unclassified command pane aborts before the launchd handoff.

hack ──► test ──► PR ──► batch-test ──► merge ──► ship ──► release
  1. hack — edit in place, or let Super c agents draft on worktree-* branches in parallel; the main checkouts never move.
  2. test./bench try builds your real machine against the local checkouts (from inside a worktree, against that branch). ./bench try switch activates it — main checkouts only.
  3. PR — push the branch and open a PR against main; never push to or git merge into main directly.
  4. batch-test./bench try-batch feels the whole open-PR queue in one rebuild, main untouched — so you verify before landing anything.
  5. merge — merge the PRs that passed (on GitHub, or gh pr merge); leave the rest open.
  6. ship./bench ship pushes upstream→downstream, rippling every flake.lock in dependency order.
  7. release./bench release <repo>; CI does the rest. Four repos are releasable: pounce (GitHub release + Homebrew formula bump), trill and perch (Homebrew cask bump + the rice’s flake pin), and nebelhaus (the tag becomes what nebelhaus.com/init.sh serves to new installs).

Each repo carries its own CLAUDE.md with the deep rules for that boundary — start there when working inside one.