marver
A TUI workspace for AI agent sessions — tmux orchestration, git worktree management, and repo control in one place.
Start a task, describe it, and marver opens a tmux session with an agent already working in its own worktree. Inspired by k9s, cmux, and Warp.
Status: early development. Every subsystem exists and the loop closes, but nothing has been used in anger yet. Expect to be the first.
marver open the interface, starting a daemon if none is running
marver status report whether a daemon is running, and what it holds
marver restart stop the running daemon and start this version
marver upgrade install the newest published marver
marver cleanup remove the worktrees left by finished tasks
marver daemon run the scheduler and hook receiver in the foreground
marver scan [root] list the git repos it can see
marver --version print the version (-v, -V)
marver --help print usage (-h)
Options, on every command: --data-dir (database, socket, log), --scan-root
(where repos are looked for), --cap (how many agents run at once), --harness
(which agent to run). The interface and daemon find each other through those
paths, so a --data-dir given to one must be given to the other.
Just run marver. If no daemon is listening it starts one, detached, the way a
tmux client starts its server — and that daemon keeps running after you close
the interface. It owns the queue: a worktree per repo, a tmux session, an agent,
and a notification when a task finishes or gets stuck.
Keys
Three rules hold everywhere:
escleaves the screen. On the task list that means quitting.ctrl-candctrl-qalso leave —ctrl-cdrops the text being typed if a field is open. In the agent's terminalctrl-cgoes to the agent;ctrl-qstill leaves.xdiscards,ccommits. Never the other way round./filters the list you are on, on every screen that has one. Typing narrows as you go,↵keeps it,escbacks out — the field first, then the filter, then the screen.
No other chord does anything: a modified key is not a letter binding.
The wheel scrolls what is tall on the screen — the agent's output, a diff —
and moves the selection on a list. marver asks the terminal for the mouse to do
that, so drag-to-select needs shift held (option in some terminals).
Without it the terminal turned wheel notches into arrow keys, which in the
agent's pane went to the agent and recalled its last prompt.
Task list
| Key | |
|---|---|
j k · ↓ ↑ |
move |
g G · Home End |
first, last |
↵ |
open the agent's terminal |
/ |
filter by title, repo, state, or prompt |
y |
what it was asked to do — any key closes it |
v |
review the diff |
n |
new task |
p |
pause it, or resume it |
f |
say its agent has finished |
t |
its todos — tab there reaches the global ones |
x |
cancel it; again once it has ended to archive it |
X |
remove a finished task's worktrees — twice if there is work to lose |
u |
put an archived task back |
s |
cycle order — newest, oldest, by repo |
a |
cycle what is shown — open, all, archived |
r |
refresh |
R |
the repos |
esc q |
quit |
New task
| Key | |
|---|---|
tab |
next field: repos → prompt → title |
/ |
filter the repo list |
space |
select a repo |
j k |
move through repos |
↵ |
queue it — from the repo list or the title |
ctrl-s |
queue it from anywhere, including inside the prompt |
esc |
discard |
↵ types a newline in the prompt rather than queueing, since a prompt is
routinely several paragraphs. The title is optional — left blank it is the
first line of the prompt, cut at a word, shown dimmed before it is taken.
Agent terminal
| Key | |
|---|---|
ctrl-q |
back to the list |
ctrl-esc |
back to the list, where the terminal can send it |
| wheel | back through the agent's output — 10,000 lines are kept |
↵ |
to the agent, and returns a blocked or reviewed task to running |
Every other key goes to the agent, esc included — it is how Claude is
interrupted. Any key also brings the view back to what the agent is printing
now, so answering after scrolling up does not leave you reading history. ctrl-esc needs the kitty keyboard protocol and will not arrive in
Terminal.app or iTerm2. ctrl-q always works.
Todos
| Key | |
|---|---|
a |
add one — ↵ keeps it, esc drops it |
/ |
filter them |
↵ |
use it: sends it to the agent, or starts a task from it |
space |
tick it off, leaving it in the list |
x |
delete it |
tab |
switch between this task's todos and the global ones |
esc q |
back |
A task's todo is typed into that agent's session. A global one opens a new task with the text as its prompt, and is ticked off only once the task is queued.
Repos
| Key | |
|---|---|
j k · ↓ ↑ |
move |
g G · Home End |
first, last |
/ |
filter by name or branch |
f |
fetch it |
p |
fast-forward it |
r |
read them all again |
esc q |
back |
Review
| Key | |
|---|---|
j k · ↓ ↑ |
move between files |
space s |
stage or unstage the file |
a |
stage everything |
tab |
switch between the staged and working-tree diff |
ctrl-d ctrl-u · wheel |
scroll the diff |
/ |
filter the files |
c |
commit — type a message, ↵ to confirm |
x |
reject — type instructions to the agent, ↵ to send |
r |
refresh |
esc q |
back |
Rejecting types your instructions into the agent's session and returns the task
to running. Instructions containing control characters are refused: \r would
submit half a sentence, \x03 would interrupt the turn being asked for.
Filtering
/ on any list narrows it as you type, k9s-style. ↵ keeps the filter and
gives the keys back, esc drops what you typed, esc again clears the filter,
and only then does it leave the screen.
It searches more than the row shows: a task matches on its title, repo, state,
or prompt, so /auth finds the task you described that way even when the
title says something else. The count in the header says what is hidden — Tasks (2 of 4 open) · /auth.
Two details worth knowing. On the new-task screen only the repo list filters,
since in the prompt / is a character you are typing — and a repo you chose
stays chosen when the filter hides it. On the repo screen the filter narrows
what you see and not what is being fetched, so a job already running still
finishes for every repo.
Agents
marver --harness claude Claude Code (the default)
marver --harness codex OpenAI's codex
marver --harness opencode:opencode anything else, by name and command
marver --harness aider:aider --yes with arguments, if it needs them
Quote an argument that contains a space — --harness 'x:prog --mode "go fast"'
— and it stays one argument all the way to the agent.
Worktrees, a queue, a session, a diff to review: none of that is specific to Claude Code. Being told what the agent did is, and it is what the states run on. So a harness is defined by what it can say:
| how it reports | what marver learns | |
|---|---|---|
claude |
a settings file of hooks | started, finished, blocked, and why |
codex |
its notify program |
finished a turn |
| anything else | nothing | nothing |
A codex task never blocks — agent-turn-complete is its only event — and
shows no token figures, which come from a transcript only Claude Code writes.
Anything else is started and then watched: everything works except a task
that finishes by itself. f on the task list is how you say it has.
Neither has its config edited. Claude Code takes --settings and codex takes
--config notify=[…], so reporting is wired on that one task's command line —
marver never writes to ~/.claude or ~/.codex.
f earns its place on Claude Code too: a Stop arriving while the daemon is
down is simply gone, since marver hook always exits 0.
The repos
R shows what every scanned repo is standing at — what a new task would start
from:
repo branch upstream default changes
api main up to date clean
marver release-prep ↑2 ↓1 ↓12 origin/main 3 files
notes detached 1 file
old-thing gone from disk
upstream is the gap to the branch this one tracks; default is the gap to
origin/main, which says whether starting here would branch from something
stale. The second is blank when it would only repeat the first. Behind is
coloured, ahead is not.
f is a plain git fetch (no --prune) and p is git pull --ff-only, both
deliberately timid: they run on one keystroke against a repo you are not looking
at and possibly one an agent is working in. Neither blocks the interface — rows
fill in as they arrive, one job at a time.
The list comes from the daemon's last scan, not a fresh walk, so it agrees with the repo picker on the new-task screen.
Tasks
y puts a task's brief over the row: repos, branch, when it was described
and when it started (two different times — the gap is the queue), and the
prompt in full. The same brief is written to task.md in the task's workspace
when it launches, so the directory says what it is for.
p sets a task aside and picks it up again. One that never started is held
out of the queue; one with a live agent is interrupted with esc and frees its
slot. Nothing confirms the interrupt — Claude reports what an agent does, not
what is done to it — so resuming types continue into the session rather than
only setting the state back.
x cancels a task that is still going. Pressing it again once the task has
ended kills its tmux session and archives the row; a cycles round to the
archive and u brings one back. Archived is a date beside the state it ended
in, not a state of its own.
Tokens. A task's screen shows opus-5 · 310k ctx · 334k out beside the repo.
ctx is a level — how full the window was on the last turn — and out is a
total across every turn. Nothing is shown until the agent has answered once,
because unknown is not zero. There is deliberately no cost figure: the
transcript records no prices, so one could only come from a hardcoded table.
Cleaning up
A task keeps its worktrees after it ends — git worktree remove --force
discards uncommitted changes, and losing unreviewed work while tidying a list
would be worse than a directory left on disk.
marver cleanup remove the worktrees of finished tasks
marver cleanup --dry-run list what would go, change nothing
marver cleanup --branches delete their branches too, where merged
marver cleanup --force remove work that was never committed
Only committed, failed, and cancelled tasks are offered; a worktree still
holding changes is listed and left alone. Branches are kept by default, since
for a committed task that branch holds the only copy of the work. Nothing runs
this on a schedule.
Upgrading
marver upgrade # cargo install marver --force
marver upgrade --restart # and swap the daemon too
Installing does not replace a running daemon: it keeps executing the old binary
and still holds the socket. That is deliberate — it is supervising live agents.
marver status says when the versions differ and marver restart swaps it,
refusing while any task is running or blocked. Agents in tmux survive a
restart either way.
What it costs
Measured on an M-series Mac, marver 0.0.20, release build. Percentages are of one core.
| CPU | Memory | |
|---|---|---|
| daemon, idle | 0.03% | 7.4 MB |
| interface, task list | 0.01% | 8.2 MB |
| interface, agent's terminal, agent working | 1.8% | |
| interface, 9 MB/s of pane output | 31% |
The daemon does nothing between events; the 60-second repo scan takes 70µs over five repos. The interface draws only when something changed. The expensive case is the VT parser, and nothing a real agent does approaches that rate.
Known gaps
- The whole loop has never been run end to end by a person, though every part is tested — 600-odd tests, several driving real tmux servers and repos.
- A dismissed permission prompt does not unblock a task. Claude Code emits
a hook when you type a reply, not when you answer a dialog, so a task
blocked on a permission prompt stays
blockeduntil the agent finishes. - Nothing reclaims a worktree on a schedule.
Xon the task list removes the selected finished task's worktrees andmarver cleanupsweeps them all, but neither happens by itself. - Two data directories cannot share a repo. Sessions carry a tag for the
data directory that made them, so two marvers on one machine no longer fight
over
marver-1— but branches are still namedmarver/<id>-<title>, so two of them starting the same-numbered task against the same repo collide on the branch. - A hook arriving while the daemon is down is lost —
marver hookalways exits 0 by design.fon the task list is the way out. - An old daemon keeps serving after an upgrade, by design.
marver restart. - A codex task never blocks and shows no tokens; an unknown harness reports nothing at all.
- A multi-repo commit is one commit per repo, sharing a message. If a
later repo fails, the earlier ones are put back with
reset --soft, so the task commits all or nothing — unless the undo itself fails, which it says. - Status messages last 500ms, errors included.
- Invalid option values are ignored silently, except
--harness. - Token figures depend on a format nobody promised to keep, so they are built to fail quietly.
Upgrading migrates the schema. An old daemon keeps working against a migrated database, but restart it when convenient.
See ARCHITECTURE.md for the design and the reasoning behind it.
Requirements
tmux 3.x, git, and an agent — Claude Code, codex, or your own.
Building it needs Rust 1.88 or newer, for let-chains and for what ratatui asks
for. An older toolchain fails partway through the dependency graph rather than
at the manifest, so rustup update is the fix.
Name
A marver is the polished steel table a glassblower rolls hot glass across, shaping the piece between trips to the furnace. It is the surface where work takes form.
License
MIT OR Apache-2.0, at your option.