documentation
revmux
A supervised multi-agent review, normally launched by your coding agent rather than typed by you.
revmux spawns claude --print and codex exec subprocesses, watches them,
and returns findings on stdout as JSON or markdown.
What revmux is #
revmux runs a review and returns findings, and does nothing else. It performs no scope detection, no
git operations, no PR fetching and no source modification. It has zero VCS dependency: no git library,
no git subprocess, no repository walking. All review context is written to disk by the
caller and passed in as a task round.
That caller is normally a model. You ask your coding agent for a review, and the shipped skill does the rest: it works out what is under review, gathers the context, writes the round, launches revmux, reads the report back and acts on it. To that agent revmux is a black box with a stable contract, context in and a verified report out, which is why the report is JSON by default and why every subcommand answers in JSON too. Running it by hand works exactly the same way and is how the pages here describe it, but it is not the usual path.
The subject does not have to be code. A round is a scope description plus whatever
context you put beside it, so a branch, a pull request, a design document, an implementation plan, a
proposal or a filed issue all go in the same way. The eight code lenses read a change; the
expert profile rates what goes wrong if the thing is built and run as written, so it
reads a plan as readily as a diff; and triage runs a four-way panel over a filed item,
where the severities rate how much a point bears on the decision rather than what breaks at runtime.
The split is deliberate. The agents themselves run diff commands inside the working directory you point them at, and revmux only substitutes a path. Anything that would make revmux read a repository belongs in the caller.
What you get over an ad hoc fan-out. A watchdog that notices a stall, a kill and retry you own, a live view of every agent with per-agent token counts, and a run archive that survives the session. The subprocess does not make the model faster; it makes the run recoverable and auditable.
Install #
Homebrew, on macOS:
brew install umputun/apps/revmux
It is a cask, so brew install --cask umputun/apps/revmux is the explicit spelling of the
same thing, and brew upgrade picks up later releases.
Binaries and packages
Every release carries prebuilt binaries for
macOS and Linux on amd64 and arm64, as .tar.gz archives plus .deb and
.rpm packages. This is the Linux path, since Homebrew casks are macOS only.
dpkg -i revmux_<version>_linux_amd64.deb rpm -i revmux_<version>_linux_amd64.rpm
From source
With a Go toolchain:
go install github.com/umputun/revmux/app@latest
The binary is installed as app, so rename it to revmux, or build from a
clone instead:
git clone https://github.com/umputun/revmux.git && cd revmux make build # produces .bin/revmux make install # and symlinks it to /usr/local/bin/revmux
make install links rather than copies, so a later make build is picked up
without reinstalling. Override the location with BINDIR when
/usr/local/bin is not writable, as in make install BINDIR=~/bin.
make uninstall removes the link.
The agent skill
The binary runs a review; the skill is what asks for one. Install it in Claude Code with two slash commands, and the repository becomes a plugin marketplace of one:
/plugin marketplace add umputun/revmux /plugin install revmux@revmux
For Codex CLI, copy the tree instead:
cp -r plugins/codex/skills/revmux ~/.codex/skills/revmux. After either, ask for a
review in words and the skill does the rest. What it does is further down.
Model CLIs
revmux drives the model CLIs as subprocesses, so whichever ones your profile names must already be installed and authenticated. Which those are is a property of the profile, not a fixed pair:
-
comprehensive,focused,final,grill-me,triage,expert: both, a claude roster plus codex claude-only: claude alonecodex-only: codex alone
preflight.sh in the shipped skill answers it for any profile and any invocation,
--lenses included.
ANTHROPIC_API_KEY is stripped from the child environment by default so
claude uses interactive subscription auth. Pass
--preserve-anthropic-api-key if you authenticate by key. CLAUDECODE is
always stripped, since a claude child refuses to start when it thinks it is a nested
session.
Quick start #
revmux new creates the round and prints every path you write to, so nothing constructs a
path by hand:
$ revmux new --task pr-123 --run 01-initial
{
"task_dir": "/abs/.revmux/tasks/pr-123",
"task_file": "/abs/.revmux/tasks/pr-123/task.md",
"round_dir": "/abs/.revmux/tasks/pr-123/01-initial",
"input_dir": "/abs/.revmux/tasks/pr-123/01-initial/input",
"scope": "/abs/.revmux/tasks/pr-123/01-initial/input/scope.md",
"goal": "/abs/.revmux/tasks/pr-123/01-initial/input/goal.md",
"profile": "/abs/.revmux/tasks/pr-123/01-initial/input/profile.md",
"context": "/abs/.revmux/tasks/pr-123/01-initial/input/context",
"created": ["task_dir", "task_file", "round_dir", "input_dir"]
}
Take the scope path out of that payload rather than joining it yourself, write the scope
into it, and run the review. The same call again is safe: a round already scaffolded is reported
rather than recreated.
scope=$(revmux new --task pr-123 --run 01-initial | jq -r .scope) cat > "$scope" <<'EOF' Review the changes on this branch against master. Diff command: git diff master...HEAD EOF revmux --task pr-123 --run 01-initial
That runs the comprehensive profile, shows a live TUI, and writes the report to stdout as
JSON. After fixing something, open a new round on the same task and revmux carries the earlier rounds
into every prompt:
revmux new --task pr-123 --run 02-after-fix # then write its own input/scope.md
revmux --task pr-123 --run 02-after-fix > findings.json
Three stages #
Only the roster and the severity bar vary between review shapes, so everything else is configuration.
-
find. The profile's roster runs in parallel: several
claudeagents, each composing one or more lenses, plus acodexpeer. Launch is staggered, agent one first and the rest released once it produces its first output. Each agent returns structured findings. -
synthesize. One model call. It merges every source's findings, dedupes on
(file, line ±2), boosts confidence where distinct sources corroborate, splits out open questions and pre-existing issues, and drops weak singletons. It is told the true source roster as data, including which agents degraded. - verify. Parallel agents grouped by directory, thin directories merged and the group count capped. Each verifier sees only its own group, so it cannot anchor on a neighbouring finding. Every finding comes back with a verdict: confirmed, refined, rejected, immaterial or pre-existing.
--verify-group-by source keys the groups by the agent that raised the finding and skips
the thin merge instead, so a panel of one-argument agents does not collapse into a single verifier.
--no-synthesis passes findings through with their attribution intact, and
--no-verify marks every finding unverified rather than silently claiming it
was checked.
Codex is a peer source, not a second pass. It runs alongside the lens agents and its findings go through the same synthesis and verification. Ordering the two would mean the second reviewer sees the first's findings and anchors on them, which is exactly what the cross-source confidence boost assumes did not happen.
Supervision and degrade #
A finder that produces no output for --idle-timeout (2m by default) is
killed and retried once. So is one that exceeds --hard-timeout (20m per attempt). On a
second failure the agent is marked degraded and the run continues: the report banner names the
missing agent, and synthesis is told the real source count rather than being left to assume a full
roster. A run where every source degraded is a tool error, not a clean empty report.
Verification is single-attempt. A verifier that fails, times out, or answers with
something carrying no verdicts leaves its own group unverified rather than being
relaunched, and a verdict the model omits or spells wrongly leaves that one finding
unverified. The findings themselves are never dropped by such a failure: silence from a
verifier is not a rejection.
Agent processes are started in their own session, so the terminal never signals them directly. A
delivered SIGINT or SIGTERM cancels the run and revmux tears each process
group down itself, rather than leaving the model CLIs and everything they spawned running
unsupervised.
What counts as a source #
A source is a process. The cross-source confidence boost counts distinct processes, never tags and never lenses. An agent carrying two lenses that flags the same issue under both is still one source, because it cannot corroborate itself.
The wire format keeps the distinction in two fields that are never interchangeable.
sources holds agent names such as ["bugs+impl", "codex"] and is the only
input to the boost. lenses holds the lens names that raised the finding and is
informational. revmux stamps sources itself once the model's output is parsed, and no
schema exposes the field, so an agent cannot name itself twice.
Task directory #
Review context reaches revmux only as a task round the caller has filled. --task names a
task under --tasks-dir (default ./.revmux/tasks), and --run
names one round inside it. Both names are caller-chosen and semantic; revmux allocates neither.
<tasks-dir>/pr-123/ a task: one subject, reviewed over as many rounds as it takes
├── task.md optional; front matter identifying the task
├── 01-initial/ a round
│ ├── input/ caller-written; the only channel review context travels through
│ │ ├── scope.md {{SCOPE}} required; missing or empty is a load-time error
│ │ ├── goal.md {{GOAL}} optional
│ │ ├── profile.md {{PROFILE}} optional, the project's own conventions
│ │ └── context/ {{CONTEXT}} optional: ticket text, design notes, spec excerpts
│ └── ... revmux-written artifacts, see the run archive
└── 02-after-fix/ the next round, with its own input/
Context belongs to the round, not to the task. Round two reviews the fixes for what round one found: a different scope, usually a different goal. Kept at task level they would be overwritten by whoever composes the next round, taking the record of what the previous round reviewed with them.
Variables expand to the paths of these files, never to their contents, and the agent
reads them itself. Prompt composition stats them and never opens one, so no prompt can be bloated by a
large scope. An absent optional file expands to none provided, which is not an error: the
run proceeds with generic severity calibration.
There are no --goal, --goal-file, --profile-file or
--context-file flags. One mechanism, no precedence rules, and nothing for revmux to
author.
--run has no default: the round holds your own context, so revmux cannot name one you
have not filled. A round that has already run is an error rather than an overwrite, because a round
that went badly is exactly the one worth keeping.
Neither name may contain a path separator or .., be absolute, or begin with a dot. A
round additionally may not be called task.md: that is the one entry the task directory
keeps beside its rounds, and a round named after it would be read as the task's own metadata.
revmux new
revmux new --task <id> --run <name> creates the task directory, a
commented-out task.md, the round and its input/, then prints every path you
write to as JSON along with a created list naming which of them this call made. It
creates the tasks root itself too, so a first run on a clean checkout materializes
./.revmux/tasks/ as well. Everything else in revmux opens and never creates, so a typo'd
--task on a review is an error rather than an empty task nobody filled.
It never overwrites. An existing task.md is left alone, and a round that has already run
is refused. A round whose review was interrupted before it finished is not one that has run: it is
scaffolded and reviewed again under the same name, with the input/ you wrote still in it,
provided that review had not already written artifacts into the round. If it had, new
refuses the name and says what is in there, so it never hands back a round the review itself would
reject.
task.md
Optional, at task level, and about the task rather than about any one round:
--- description: OAuth token exchange rework url: https://github.com/umputun/revmux/pull/123 branch: feature/oauth base: 4ed3259 --- Reviewing the token exchange path after the provider swap.
Every key is optional, as is the body and the file itself. revmux config reports the
front matter under paths.tasks, which is how a caller matches an existing task instead of
guessing at an id. Opening pr123 beside an existing pr-123 silently forks
the history into two.
revmux stores and reports these; it never resolves one. No git command runs against
branch or base, and nothing is fetched from url. They are
strings you wrote and strings you read back.
Prior rounds
Prior rounds are injected into every composed prompt. revmux wrote them, so it hands them over rather than making the caller copy them forward. The injected block is the task directory path plus a generated one-line inventory per round: name, when it ran, finding counts by severity, and which sources degraded. An agent can judge relevance without opening anything, and read a round in full when that matters.
The block carries its own re-evaluate-independently instruction, and on a first round it is omitted
entirely. It is not a {{VAR}}, because a variable would be opt-in per file and any lens
or profile omitting it would silently lose the history.
Two precedence chains #
Runtime knobs resolve from the command line, then ./.revmux/config, then
~/.config/revmux/config, then the built-in default. Layers merge per key, so a project
config setting one knob leaves the rest alone. The project layer is auto-detected: no flag selects it,
and its absence simply drops it.
Prompt and lens files resolve from ./.revmux/, then
~/.config/revmux/, then the defaults built into the binary, per file.
Overriding one lens leaves every other lens where it was, and deleting an override falls back to the embedded
copy rather than disabling the lens. To actually drop a lens, remove it from the profile roster.
The project layer is a review standard you can commit
What a project actually cares about, its conventions, what counts as major, the mistakes it keeps
repeating, usually lives in a maintainer's head and reaches contributors one review comment at a
time. revmux init writes it to disk instead: the config template plus every prompt file
as it currently resolved, ready to edit.
Checked into .revmux/, that tree is the project's review, versioned and diffable like
the rest of the code. Everyone who clones the repository runs it, so a contributor gets the review a
maintainer would have run before the maintainer opens the pull request. A finding traces back to the
lens text that raised it rather than to a prompt nobody kept. A review that missed something is fixed
by editing a file, once, and every later round has the fix. What belongs there is anything a reviewer
would otherwise have to be told: a lens for a subsystem with its own failure modes, a severity bar
that says what major means here, a profile shaped like the review this project actually runs.
.revmux/ is code. The project layer supplies prompt text as well as knobs, and
that text becomes the instructions a headless agent with a shell executes. Running revmux inside a
repository trusts it the same way .claude/ or a Makefile there does. Review
it before reviewing a branch you did not write, or run revmux from outside the tree: the project
layer is read from the process working directory, never from --workdir.
~/.config/revmux/
├── config INI, runtime knobs only
├── prompts/
│ ├── profiles/
│ │ ├── comprehensive.md roster front matter + shared preamble + severity bar
│ │ ├── focused.md final.md claude-only.md codex-only.md
│ │ └── grill-me.md expert.md triage.md
│ ├── synthesis.md
│ └── verify.md
└── lenses/
├── bugs.md impl.md architecture.md
├── quality.md docs.md tests.md comments.md adversarial.md
└── grounding.md precedent.md thesis.md antithesis.md cost.md
--config-dir relocates the user layer. revmux init
materializes ./.revmux/ from whatever resolved, and
--dump-defaults <dir> extracts the embedded prompt tree instead, which is how a
customized file is diffed against the shipped one. Neither overwrites a file you have customized, and
a normal run writes no config at all.
Paths resolve against the process working directory: the project config layer, and
--tasks-dir's ./.revmux/tasks default. --workdir is separate,
setting where the subprocesses run and what {{WORKDIR}} expands to. Reviewing a repository
from outside it means passing --config-dir and --tasks-dir as well.
Profiles #
A profile is roster front matter plus a body that is the shared preamble and severity bar. The
top-level model is the review's runner; a roster entry or a stage naming its own overrides
it.
---
description: all eight lenses across three claude agents plus an adversarial codex peer
model: claude/opus:high
agents:
- {name: bugs+impl, lenses: [bugs, impl], color: cyan}
- {name: arch+quality, lenses: [architecture, quality], color: magenta}
- {name: docs+tests, lenses: [docs, tests, comments], color: green}
- {name: codex, lenses: [adversarial], model: codex/gpt-5.6-sol:high, color: yellow}
---
The top-level runner is inheritance, not a fixed review topology. This supports single-vendor profiles when only one CLI is available, mixed peers for independent perspectives, a wide lower-effort finder roster followed by a stronger synthesis model, or a high-effort verifier where false positives are expensive. Each roster entry remains a distinct source regardless of which binary runs it, while its lenses define the job independently of the runner.
The model string
One model string selects the binary, the model and the effort together. The binary leads
and is mandatory, either claude or codex, so a value validates itself and
revmux never has to guess which CLI runs gpt-5.6-sol from a catalog of model names that
would go stale.
claude claude, its own default model and effort claude/opus:high fully specified codex/gpt-5.6-sol effort falls back to the profile's, then the binary's codex:high codex's default model at high effort
The three travel together because they are not independent: opus means nothing to codex.
A file cannot state a pairing that will not run, and an entry naming a different binary than the
profile brings its own model rather than inheriting one belonging to the other. A trailing slash is
refused, since claude/ is a second spelling of claude. It parses on the first
/ so a model whose own name has one survives, and on the last :, whose
suffix must be a real effort: :hgih is a load error rather than a typo nobody sees.
A stage resolves through three layers in turn: its stages: override, the stage file's own
model:, then the profile's. The shipped synthesis.md and
verify.md name no runner of their own, so codex-only is one line and no
more. The optional stages block is for a deliberately mixed run, such as codex finders
and a claude synthesis:
stages: synthesis: claude/opus:high
Everything is validated at load. An unknown binary, effort, lens or color is a startup error, never a silent default: a typo'd model quietly changing which model reviews your code is worse than a failed launch.
Shipped profiles
| profile | roster |
|---|---|
| comprehensive |
bugs+impl, arch+quality, docs+tests on claude, the last
carrying comments too, plus an adversarial codex peer
|
| focused | one bugs agent plus the codex peer, for a small or time-boxed change |
| final | bugs+impl plus the codex peer, nothing below major reported |
| claude-only | the same four lens splits on claude, no codex peer, for a machine with no codex |
| codex-only | the same four lens splits on codex, and synthesis and verify with them, no claude anywhere |
| grill-me |
bugs+impl and architecture+quality, each run once on claude and once
on codex, every agent reading against the change
|
| expert |
two agents at the highest effort, codex gpt-5.6-sol:xhigh and claude
fable:xhigh, each carrying all eight lenses, both stages on fable
|
| triage |
facts, thesis, antithesis on claude plus
cost on codex: a panel over a filed item rather than a diff, and it wants
--no-synthesis
|
expert is for when the answer matters more than the wall clock, and it is expensive
enough to be worth asking for deliberately. Both agents read everything rather than splitting the
lenses, so where the two models independently agree the cross-source boost measures genuine
corroboration rather than two halves of one review. Its severity bar rates what goes wrong if the
thing is built and run as written, so a plan reads as naturally as a diff.
Writing your own
The eight are starting points, not the menu. A profile is a file under
prompts/profiles/ in any of the three layers, so dropping
.revmux/prompts/profiles/release.md into a project makes
--profile release work, with no registration step anywhere. The same is true of lenses:
a roster naming payments resolves lenses/payments.md through the same
chain, and a file only your project has resolves in your project.
---
description: pre-release pass over the payment path
model: claude/opus:high
agents:
- {name: money, lenses: [bugs, impl, payments], color: red}
- {name: contracts, lenses: [architecture, docs], color: cyan}
- {name: peer, lenses: [adversarial], model: codex/gpt-5.6-sol:xhigh}
- {name: second, lenses: [bugs], model: codex/gpt-5.6-sol:high}
stages:
synthesis: claude/opus:high
verify: claude/sonnet:low
---
Everything below the front matter is the preamble every agent in this roster
receives, including what counts as critical, major and minor here.
Four sources, two vendors, four different jobs, and a synthesis model stronger than the verifier after it. There is no fixed shape to a review: the roster is as wide as you are willing to pay for, each entry carries as many lenses as the job needs, and any entry can leave the profile's model for its own. Copy a shipped profile and edit it rather than starting from an empty file, since the body carries the severity bar the findings are calibrated against.
triage reviews an issue, a proposal or a discussion instead of a change. Its severities
rate how much a point bears on the decision rather than what goes wrong at runtime, and it returns
arguments for a maintainer to weigh. revmux decides nothing. Run it with
--no-synthesis: every argument on a four-way panel is single-source by construction, so
the drop rule eats the minor ones and the confidence boost fires on agreement between agents told to
disagree. --verify-group-by source keeps each panelist's case in front of its own
verifier.
Lenses #
Executor and lens are orthogonal. Every roster entry composes lenses, and its model: only
selects which binary runs it. There is no codex-specific prompt file: codex is an entry whose
model: names it, composing lenses/adversarial.md, so the adversarial lens
runs on claude by changing one word and bugs runs on codex the same way. Lens text stays
executor-agnostic, and the output-contract difference (claude has --json-schema, codex
does not) is injected by the executor.
| lens | covers |
|---|---|
| bugs | correctness defects: logic and boundaries, nil and bounds, concurrency, resource lifetime, error handling |
| impl | goal fit: whether the change does what it set out to do, is wired up, and is proportionate |
| architecture | conventions and organization: the project's own rules, established patterns, dependency and interface shape |
| quality | style, over-engineering, error handling and accidental duplication in code that already works |
| docs | documentation accuracy: doc comments against the code, and the project docs the change leaves stale |
| tests | whether tests exist where a defect can hide, actually exercise the code, and survive concurrency |
| comments | the code's own stated rules: doc comments and inline notes the change was supposed to obey |
| adversarial | attacks the change looking for what a sympathetic reader would accept |
| grounding | whether what a filed item claims is true of the code as it stands today |
| precedent | how comparable asks were decided here before, and whether that bears on this one |
| thesis | the strongest honest case that a filed item should be done or that its report is real |
| antithesis | the strongest case against, and whether something simpler reaches the same goal |
| cost | what implementing a filed item reaches into, and whether the work is proportionate |
The last five read a filed item rather than a diff and are what the triage profile
composes; the eight above them review a change.
--lenses bugs,impl replaces a profile's roster while keeping its body. It produces
one agent carrying every named lens, not one agent per lens: a caller asking for two
lenses is asking for a viewpoint, not for two corroborating votes. The synthesized entry inherits the
profile's top-level model whole, binary included, so
--profile codex-only --lenses bugs runs on codex.
Prompt composition #
One agent's prompt is the profile body plus each of its lens files, concatenated, with
{{VAR}} substituted and the prior-rounds block appended. The variable vocabulary is
closed: {{SCOPE}}, {{GOAL}}, {{PROFILE}},
{{CONTEXT}}, {{WORKDIR}}, plus {{FINDINGS}} for both model
stages and {{SOURCES}} for synthesis only. Verify sees one group at a time and is never
given the roster. A prompt file naming anything else fails at load, which is what makes a typo loud
instead of silent.
Runtime knobs #
These read from the config file under the same name as the flag. The reference carries the full flag list.
| flag | default | meaning |
|---|---|---|
| --idle-timeout | 2m | kill and retry an agent after this long with no output |
| --hard-timeout | 20m | kill an agent after this long, per attempt |
| --stagger-delay | 30s | how long to wait for the first agent before releasing the rest |
| --max-parallel | 4 | how many agents run at once |
| --verify-groups | 6 | cap on the number of verifier groups |
| --verify-group-by | dir | key verifier groups by directory or by the agent that raised the finding |
| --tasks-dir | ./.revmux/tasks | root directory holding task directories |
| --auto-exit | 0s | close the terminal UI this long after the report arrives; 0 never closes it |
| --profile | comprehensive | profile naming the roster to run |
--task and --run are both required for a review, and neither is a config
key: a config file naming the round to write would make the same command review different context in
different directories.
The report #
The report goes to stdout as JSON, or as markdown with --markdown. The
TUI renders to the tty and progress lines go to stderr, so
revmux --task pr-123 --run 02-after-fix > findings.json works with the display running. The TUI is gated
on the tty being openable, never on stdout being a terminal, which is false in exactly that
invocation.
{
"scope": {"task": "pr-123", "run": "02-after-fix",
"scope_path": "/abs/.revmux/tasks/pr-123/02-after-fix/input/scope.md"},
"sources": {
"expected": 4, "reported": 3, "degraded": ["docs+tests"],
"agents": [
{"name": "bugs+impl", "lenses": ["bugs", "impl"], "executor": "claude",
"requested_model": "opus", "actual_model": "claude-opus-5",
"effort": "high", "tokens": 48210, "raised": 6, "degraded": false}
]
},
"findings": [
{"id": "f1", "file": "app/pipeline/find.go", "line": 88, "end_line": 0,
"severity": "major", "confidence": 90,
"title": "...", "body": "...", "fix": "...",
"sources": ["bugs+impl", "codex"], "lenses": ["bugs", "adversarial"],
"verdict": "confirmed"}
],
"open_questions": [], "pre_existing": [], "immaterial": [],
"stats": {
"started_at": "2026-07-26T16:02:11Z", "finished_at": "2026-07-26T16:07:44Z",
"duration_ms": 333000, "tokens": 184920,
"stages": [{"name": "find", "duration_ms": 201000},
{"name": "synthesis", "duration_ms": 62000,
"executor": "claude", "model": "opus", "effort": "high"}]
}
}
line is the anchor and end_line is optional: zero means a single line, and a
zero line means a file-level finding that renders as the bare path.
verdict is one of confirmed, refined, rejected,
immaterial, pre_existing, or unverified when nobody checked it,
whether because the stage was skipped or because that group's verifier failed. Empty lists are emitted as arrays rather than null.
--min-confidence filters once, before anything renders, and the printed report, the
findings browser and the exit code are all computed from the filtered set. A finding the exit code
says is absent is never listed in the TUI. Open questions, pre-existing and immaterial findings pass
through untouched.
Exit codes #
| code | meaning |
|---|---|
| 0 | no findings above --min-confidence |
| 1 | findings above --min-confidence. A normal outcome, not a failure |
| 2 |
tool error: bad config, unreadable prompt tree, an omitted --run, a round with no
input/ or an empty scope.md, a round that has already run or is being
written by another run, an unwritable run artifact, or every source degraded
|
The subcommands use the same 2 for their own tool errors and never exit 1:
there is no report and so no threshold to be above. A run that exits 2 usually leaves no
report, the exception being a failure writing the report to stdout, which happens after the round is
archived.
A configuration error is caught before the round is claimed at all, so it leaves no
manifest.json and the name is free. Anything that fails once the pipeline has started
leaves an empty marker beside what it had written, and both revmux new and the run itself
refuse the name and say what the round holds. Either way the input/ you wrote is
untouched.
Run archive #
Every run writes its artifacts into its own round directory, beside the input/ it was
pointed at. They exist so a review can be audited without re-running it, which the final report alone
cannot support, and because the round holds its own context, one round read in isolation shows both
what was reviewed and what came back.
<tasks-dir>/pr-123/02-after-fix/ ├── input/ the scope, goal, profile and context this round was reviewed against ├── manifest.json roster, prompt provenance and hashes, requested vs actual model, timings ├── prompts/ │ ├── agents/ composed prompt per agent, post-substitution: the bytes the model saw │ │ ├── bugs+impl.md │ │ └── codex.md │ └── stages/ separate from agents/ so an agent named `verify` cannot collide │ ├── synthesis.md │ ├── verify-app-executor.md one per group, directories by default │ └── verify-app-pipeline.md ├── stages/ a skipped stage writes no snapshot │ ├── 1-found.json findings as the find stage left them │ ├── 2-synthesized.json │ └── 3-verified.json ├── events.jsonl revmux's own decisions: stalls, retries, degrades, stage transitions ├── agents/ verbatim tees; own subdir so an agent named `events` cannot collide │ ├── bugs+impl.jsonl claude stream-json │ ├── bugs+impl.retry.jsonl a retried agent keeps both attempts │ └── codex.log codex prose ├── report.md the filtered report rendered as markdown, what --markdown writes └── findings.json
manifest.json records which of the three precedence layers supplied each prompt file and
its content hash, because two rounds of one task can use different lens text. It also records
requested-vs-actual model per agent: claude --model can be silently ignored, so a
roster's model pin is a claim until it is read back.
It doubles as the marker claiming the round. It is created exclusively as the run starts, which is both how a round that has already run is detected and how a real round is told from a directory left under the task. It is created empty and filled in when the run finishes, so a marker still empty means the run never came back, and such a round is not counted as a prior round in the meantime.
A round like that is re-runnable under the same name only while nothing else was written into
it, which is narrower than it sounds: the pipeline opens events.jsonl before it
launches an agent, so a review interrupted at any point after it started has written something. What
is re-runnable is a round claimed by a run that died before the pipeline began. The error names what
it found, nothing is deleted to make the round usable, and the fix is to open the next round and copy
the input/ across.
A round already being written by another revmux is refused too. An empty marker is what an interrupted run leaves and what a run starting right now leaves, so size alone cannot tell them apart. revmux holds an exclusive OS-level lock on the marker for the run's lifetime, and the lock is gone the moment the holding process is, so a round nobody is writing is still re-runnable with nothing to clean up.
A failed archive write fails the run. A report emitted next to a half-written archive reads
as complete, and the gap only surfaces later when someone tries to audit it. The one exception is a
per-agent tee under agents/, which degrades that one source instead: it belongs to that
one agent and is the only artifact whose failure is attributable to a single source.
Rounds accumulate and are never pruned. revmux stats reads them back
as numbers, and revmux cleanup is the one command that removes
anything.
Terminal UI #
A status table on top, one row per supervised process with its name, state, elapsed time and last activity, and one detail pane below it. The roster fills it first, and the synthesis and verify processes take rows of their own as they start, so the table shows what is running rather than only what the profile named.
The findings count in the header follows the same logic: the finders add to it, a later stage's merged
count replaces it, and it is rebuilt from the finished report at the end, since verify rejects
findings and --min-confidence filters without either emitting an event. It is shown
broken down by severity when the width allows, and colored by the worst severity in it. Red on any
critical, yellow on any major, green only when nothing above minor was found.
Tab 1 all is the combined chronological view and is focused by default; the tabs after it
are per-agent full-detail scrollback. On completion the model switches to the findings browser, and
the agent tabs stay reachable so a reader can check why a finding was raised. Each finding's body and
fix render as markdown documents.
Press i to replace those panes with the inputs captured when the TUI started. The status
table remains visible, and the input tabs show scope, goal,
profile, then each file under context/. Markdown files render as documents up
to 64 KiB per file; a larger one falls back to the line-at-a-time rendering the log panes use. The
snapshot is read after the tty opens and before any review process starts, and it does not refresh
during the run. Headless runs read no snapshot at all.
| keys | action |
|---|---|
| tab shift+tab ← → h l | switch pane |
| 1-9, then a letter | focus that pane directly; the token is shown on the tab |
| f | jump to the findings browser |
| i | show the startup input snapshot, or return to the review panes |
| ↑ ↓ k j | scroll |
| pgup pgdn ctrl+b ctrl+f | page |
| home end g G | top, bottom |
| / | filter findings; enter accepts, esc clears |
| esc | return from the input viewer, or abandon a filter; never quits |
| q | quit, once the report is in |
| ctrl+c | quit, at any point |
Only ctrl+c ends a review that is still running. q waits for the report, so a reader who reaches for it as a pager key does not lose the view of a live run. Quitting stops watching the run, it does not stop the run: the report is still written to stdout when the pipeline finishes.
With --no-tui, or when the tty cannot be opened, the same events render as timestamped
lines on stderr, each agent in its own color:
16:02:11 bugs+impl started [bugs, impl] 16:02:19 arch+quality reading the roster resolution path 16:04:02 docs+tests retrying: agent docs+tests stalled 16:05:12 bugs+impl done, 6 findings 16:05:40 ── synthesis ── 16:09:03 ── complete ── 16:09:03 6m52s, sources 4/4, degraded none 16:09:03 6 findings: 1 major, 5 minor
The closing three lines are written after the last event, to say what the run came to. They carry counts only: the findings themselves go to stdout, and a degraded run names its missing sources here rather than leaving the log looking like a complete one.
Ctrl-C delivers a signal under --no-tui. While the TUI is running it does not:
the terminal is in raw mode, so the keystroke reaches revmux as a key rather than a signal. A second
Ctrl-C, once the TUI has restored the terminal, cancels.
revmux config #
revmux is normally driven by a caller model, so the resolved configuration is machine-readable rather
than something to reconstruct from --help and a directory listing.
revmux config prints it as JSON on stdout and exits 0. It runs no pipeline
and creates nothing; the only thing it touches is a read of the tasks root.
It reports what resolved, never what is embedded: a user who overrode one lens and added another sees his own tree. Each runtime knob carries the precedence layer that supplied it, so a caller can tell a deliberate choice from a default. Flags may precede the subcommand, which is how a caller asks what a given invocation would resolve to:
$ revmux --stagger-delay=45s config
{
"knobs": [
{"name": "stagger-delay", "value": "45s", "source": "flag"},
{"name": "max-parallel", "value": 2, "source": "project"},
{"name": "profile", "value": "comprehensive", "source": "default"}
],
"profiles": [{"name": "comprehensive", "description": "...",
"runner": {"executor": "claude", "model": "opus", "effort": "high"},
"roster": [...], "stages": [...]}],
"lenses": [{"name": "adversarial", "description": "..."}],
"stages": [{"name": "synthesis", "description": "..."}],
"vocabulary": {"executors": ["claude", "codex"],
"efforts": ["low", "medium", "high", "xhigh", "max"]},
"paths": {
"tasks_dir": "/abs/project/.revmux/tasks",
"config_dir": "/home/user/.config/revmux",
"project_dir": "/abs/project/.revmux",
"workdir": "/abs/project",
"tasks": [{"id": "pr-123", "description": "OAuth token exchange rework",
"url": "...", "branch": "feature/oauth", "base": "4ed3259",
"rounds": ["01-initial", "02-after-fix"]}]
}
}
The top-level stages array is the stage prompt itself, with a runner only if that file
authored one, which the shipped pair do not. What actually runs is
profiles[].stages, and each profile also reports its own base runner as
profiles[].runner: the one the roster falls back to and the one the single agent
--lenses synthesizes runs on. That is why a preflight check can tell which binaries an
invocation needs.
paths.tasks is the task store: every task that already exists, whatever its
task.md says about it, and the rounds recorded under it. Rounds are those that ran to
completion, so neither a directory prepared but not yet reviewed nor a round whose review was
interrupted is one. An empty list always means empty: a tasks root that could not be read is reported
as paths.tasks_error, and nothing that failed is reported as nothing being there.
revmux init #
revmux init materializes ./.revmux/ so there is something local to edit: the
commented-out config template, plus every prompt file as it currently resolved.
--init is the same implementation behind a flag, for a caller that already builds an
argument list.
What it writes is the winning layer's own bytes, front matter included. A user with
~/.config/revmux/ overrides gets those copied down rather than the shipped text, so
editing the result changes the review that already runs instead of reverting it to the default one.
--dump-defaults <dir> is the other direction, and the only way to reach the embedded
copy for a diff.
{
"dir": "/abs/project/.revmux",
"config": "/abs/project/.revmux/config",
"files": [
{"path": "/abs/project/.revmux/lenses/bugs.md", "layer": "user", "created": true},
{"path": "/abs/project/.revmux/prompts/synthesis.md", "layer": "embedded", "created": true}
]
}
layer is where the content came from: project, user or
embedded. created is false for a file already there, which is reported and
left byte-identical, so a second run changes nothing and no prompt file you customized is ever
overwritten.
The config is reported as a path alone because it is not materialized the same way. It ships commented out, and one holding no uncommented key is rewritten with the current template, which is what lets an upgrade move a default you never set. A config carrying an actual setting is left exactly as it is.
revmux stats #
revmux stats reads what past rounds produced and prints it as JSON on stdout. It runs no
pipeline, spawns no agent and writes nothing: it is arithmetic over the archive, so it is always safe
to call.
$ revmux stats # every task under the tasks root $ revmux stats --task pr-123 # one task { "tasks": [ {"id": "pr-123", "description": "the auth refactor", "rounds": 5, "size_mb": 6.6, "last_run": "2026-07-27", "skipped": [], "agents": [{"name": "bugs+impl", "raised": 8, "survived": 8, "corroborated": 5, "degraded_rounds": 0, "retries": 0, "tokens": 10441185}], "lenses": [{"name": "bugs", "raised": 14, "ambiguous": 3, "verdicts": {"confirmed": 4, "refined": 6, "unverified": 4}}], "stages": [{"name": "synthesis", "in": 62, "out": 46}, {"name": "verify", "in": 46, "out": 46}, {"name": "report", "in": 46, "out": 46}]} ], "totals": {"rounds": 5, "size_mb": 6.6, "last_run": "2026-07-27", ...} }
Per agent. raised is what it put on the table before synthesis merged
anything; survived is what was still there in the round's last stage snapshot, counted
across all four of that report's arrays; and corroborated is the subset of those another
agent independently reached. The attribution is exact rather than model-supplied.
Per lens. raised counts the find stage only, since after synthesis a
finding's lenses is a union across merged findings from different agents.
ambiguous is the part of it attributable only by the raising agent's whole lens set, which
is what the find stage falls back to when the model named no valid lens. A per-lens number is only as
good as its ambiguous share, so the two belong together wherever either is quoted.
A lens whose raised sits well above its verdict total lost findings somewhere between the
two, but not necessarily to the verifier: synthesis merging two findings that carry the same lens
produces the same gap. Read it as attrition to look into rather than as rejections counted.
Per stage. in and out for synthesis,
verify and report, each the union of that report's four finding arrays.
report carries the --min-confidence attrition, and there is no
find entry since nothing goes into it. reclassified and
refined are there because in and out understate verification
badly: a finding moved into immaterial or pre_existing leaves the total
unchanged. Over one corpus verify dropped 2 findings of the 150 that reached it while lowering the
severity of 28.
Per task. size_mb is what the task occupies, summed from file sizes
rather than disk blocks. last_run is the finished_at of the newest round's
manifest.json, so it says when the task was last reviewed rather than when anything last
touched the directory. A round skipped because its artifacts would not decode is named in
skipped, with the artifact and the reason, so a corpus that shrank does not read as a
corpus that is simply smaller.
Every survivor and every per-lens number comes from the per-stage snapshots under
stages/, never from the round's findings.json, which is the filtered report.
Two numbers come from elsewhere and say so: the report stage entry reads
findings.json precisely to measure what the filter removed, and retries
comes from events.jsonl.
revmux cleanup #
revmux cleanup --task <id> removes one task and everything under it, and prints what
went as JSON. It is the only thing in revmux that deletes anything: a review, new,
init, config and stats remove nothing, so nothing is ever
removed as a side effect of doing something else.
$ revmux cleanup --task since-1f21e93
{
"tasks_dir": "/repo/.revmux/tasks",
"removed": [{"id": "since-1f21e93", "rounds": 5, "size_mb": 6.6}],
"total_mb_after": 6.4
}
The archive grows by roughly half a megabyte per round and revmux never prunes on its own, so
reclaiming is a decision rather than a policy. There is no age threshold, no size cap and no all-tasks
form. What to remove is read off revmux stats, which reports every task's size, round
count, description and date.
It removes a whole task, never a round inside one. A task's rounds are one review's
history and are read together; a task that quietly lost its early rounds would keep being reported by
revmux stats as the whole record.
It refuses more than it removes. A name that is not one task directly under the tasks root is
an error and nothing is removed. An absent --task names the flag rather than meaning
every task. A task a running review holds is refused, though that is a check taken as it goes rather
than a lock held across the removal, so do not run it against a task under review.
Agent skills #
revmux is built to be driven by a caller model, and the repository ships that caller as a skill for
two harnesses. Ask for a review in words and the skill does the rest: it resolves what is being
reviewed, runs the git commands, writes the round's input/, launches revmux, reads the
JSON back, and opens a new round on the same task after fixes.
| harness | location | install |
|---|---|---|
| Claude Code | .claude-plugin/skills/revmux/ |
/plugin marketplace add umputun/revmux then
/plugin install revmux@revmux
|
| Codex CLI | plugins/codex/skills/revmux/ |
cp -r plugins/codex/skills/revmux ~/.codex/skills/revmux |
Asked for a pull request, the skill fetches the head into a throwaway worktree, points
--workdir at it while running from the main checkout, so the archive outlives the
checkout and the branch's own .revmux/ never loads, and removes both the worktree and the
temp branch afterwards. Asked to triage a filed item instead of a change, it gathers the issue or
discussion, its thread and the author's history into context/, runs the
triage panel over them and puts the maintainer's six answers to him with the arguments
behind each.
Both trees carry the same reference material and the same scripts:
- preflight.sh
- check revmux plus the binaries a given profile and invocation need,
--lensesincluded - task-state.sh
-
resolve the tasks root from
revmux configand report what a task holds: itstask.mdanchors, its rounds, and each round'sinput/state - launch-revmux.sh
- run revmux with its TUI in a terminal overlay (agterm, tmux, Zellij, herdr, kitty, wezterm, cmux, ghostty, iTerm2, Emacs vterm), returning the report on stdout and revmux's own exit code
- analyze-corpus.py
- read the run archive and report what it says about the review itself: which stage is filtering, which lens rates hardest, whether the gating count converges
The launcher exists because an agent's shell has no tty, so the TUI never appears there. The overlay
is how a user watches a review happen; everything else about the run is identical. It forwards
PATH into the overlay deliberately, since revmux spawns claude and
codex itself and an overlay shell inherits an environment that predates the user's shell
rc files. ANTHROPIC_API_KEY is not forwarded, since an env KEY=VAL prefix
would put it in the process argv.
Development #
make build # build .bin/revmux make install # symlink .bin/revmux into $BINDIR (default /usr/local/bin) make test # race detector plus coverage, mocks excluded make lint # golangci-lint plus shellcheck over the shipped scripts make fmt # gofmt and goimports
No test spawns a real model. The executors are driven through a mocked CommandRunner
against recorded CLI fixtures, the pipeline through mocked runners, and the TUI through synthetic
bubbletea messages. revmux is MIT licensed;
the source is on GitHub.