Important
This library is under development and still being dogfooded. The documentation below is AI-generated and not reviewed yet.
Feel free to use or explore (and if you do please let me know what issues you run into!), but please know this is mostly here for the sake of building in public and not considered ready for release yet.
Manage many concurrent coding-agent sessions against one repo, and composite all their in-flight work into one live, running dev server. Review by using the app, not reading diffs. Version control (jujutsu) is the storage engine and stays invisible.
See docs/SPEC.md for the vision, docs/REVIEW.md
for the (evidence-backed) design review, docs/PLAN.md for the
implementation plan, and docs/concurrency-notes.md
for the daemon's concurrency model.
Status: M0–M4, verified end-to-end locally. Every milestone's exit criterion is a scripted, hermetic test and all are green — the M1 loop (
bb e2e), M2 crash-recovery (bb recovery), the M3 conflict/absorb/ exclusive flows (bb conflict), and the M4 status pane driven by real tmux keystrokes (bb tui) — through the real daemon, real tmux, and a real dev server, with scripted fake agents. SeeTESTING.mdfor exactly what is and isn't proven (a liveclaudesession and the F8 agent-done hooks are the notable not-yets).
- babashka (
bb) — latest (≥ 1.12.218; pinned via:min-bb-versioninbb.edn). 1.12.215+ is what makes the M4 status pane (charm.clj) work on bb's bundled JLine3; we just track latest so it's never a question. - jujutsu (
jj) ≥ 0.43 tmux≥ 3.4- your agent CLI (
claude/codex) and a dev server in the target project
Install/upgrade babashka to the latest:
brew install borkdude/brew/babashka # macOS/Linux (Homebrew)
# or: bash < <(curl -s https://raw.githubusercontent.com/babashka/babashka/master/install)
bb --version # expect ≥ 1.12.218
cd your-project
bb --config /path/to/compositor/bb.edn comp init # from source
# or, once installed via bbin/Homebrew:
comp init # interview → plan → confirm → daemon + dev server + ui
tmux -L compositor attach # the 'ui' window:
comp init tells you exactly what it will change before doing it: it
detects :trunk from your checked-out branch (never a hardcoded main) and
the dev command from the project (package.json/bb.edn/justfile/Makefile),
asks three questions when there's no config yet, preflights every command
through your login shell, and checks that the dev server didn't die on the
spot — if it did, you get its output, not a silent missing window. Everything
it creates is inventoried, and comp destroy exactly inverts it (tmux,
workspaces, unlanded commits, the config alias, the state dir — landed work
is trunk history and is never touched). comp destroy --full also removes
.jj/ when init created it. Until --full, jj's op log can restore anything
destroy abandoned.
One sharp edge to know: agent/dev commands run non-interactively through your
login shell — PATH and exported env apply, but aliases and functions from
.zshrc don't. Put env inline instead, e.g.
:agent-cmd "CLAUDE_CONFIG_DIR=$HOME/.claude-kira claude {prompt}". Every
window's output is captured to <state>/logs/<window>.log (comp config
prints where that is).
compositor ● daemon
id state on? change intent
1 running ▣ xqrnzxlrtkrp add a dark mode toggle
> 2 running ▣ yotpstloxupq make the footer sticky
↑/↓ select · n new · t toggle · K keep · D drop · s stack · a absorb · u undo · q quit
The status pane is the cockpit. n starts a take (type the intent, the
agent spawns in its own window); t toggles the selected take in and out of
the running app; K lands it on trunk; D abandons it; when two takes
collide the pane says so on both rows — s stacks the selected take onto
another (press the target's id), a absorbs your hand-tweaks from the dev
app back into whichever take owns them, u undoes the last command. The
same verbs exist as CLI commands (they drive the same daemon):
comp new "add a dark mode toggle"
comp ls # sessions, applied state, ⚠ collision badges
comp toggle 2 # the running app changes under your cursor
comp keep 1 # land it — the app doesn't flinch
comp stack 3 2 # takes collided? re-parent 3 onto 2, markers → its agent
comp send 3 "resolve the conflict markers, keep both behaviors"
comp attach 3 # hand your terminal to that agent — images, paste, the lot
comp absorb # hand-tweaks in the dev app flow back into their takes
comp undo # restore the state before the last comp command
comp status # megamerge + collisions + last op, as data
comp ui # the status pane, in whatever terminal you're in
comp config edit # this project's config.edn, in $EDITOR
comp destroy # take it all down again (--full: de-colocate too)
The tmux session (tmux -L compositor) holds everything: window 0 the
daemon, dev the dev server, ui the status pane, and one window per
agent — switch to a take's window to watch or talk to its agent.
If the daemon is down, any comp command starts it (tmux window 0) and
retries; reconcile() on boot makes that safe at any moment — after a crash,
a kill -9, or a stray jj undo behind its back, comp ls tells the truth.
cd compositor
bb test # unit tests (queue serialization, config, graph)
bb integration # jj/tmux command-sequence test (needs jj + tmux)
bb e2e # M1 exit criterion, live: daemon + tmux + dev server + fake agents
bb recovery # M2 exit criterion: kill -9 + sabotage, reconcile repairs
bb conflict # M3 exit criterion: collide/stack/resolve, absorb, exclusive+DB hooks
bb tui # M4 exit criterion: drive the status pane with real tmux keystrokes
bb destroy-test # init↔destroy symmetry: interview, detection, full/default destroy
bb lint # clj-kondo (zero warnings)
bb gen-script # emit the standalone ./compositor uberscript (inlines charm.clj)
src/compositor/
main.clj CLI dispatch (ui loads lazily so plain calls stay fast)
setup.clj init (interview/plan/preflight) + destroy, exact inverses
client.clj socket transport shared by CLI + tui; auto-starts the daemon
tui.clj the status pane: charm.clj (Bubble Tea for Clojure), Elm-shaped
jj.clj the single jj contract: templates→data, the serialization queue
tmux.clj socket-scoped tmux driver
graph.clj megamerge lifecycle (create once, rebase -s in place) + collision badges
session.clj create / toggle / keep / drop / stack / send / absorb
daemon.clj core.async loop: jj queue + watch + socket + undo
reconcile.clj repo → (store, world) repair; runs on every daemon boot (F10)
store.clj session records (EDN now, sqlite pod when the schema settles)
config.clj project config + paths
gen_script.clj uberscript emitter