Skip to content

Review units

Turn a tangled diff into a stack you can actually read — without touching a branch.

A one-shot agent session tends to land as one big tangled diff: a refactor, the feature it enabled, its tests, and a lockfile bump, interleaved across files in alphabetical order. Stacked-PR tooling solves that readability problem by materializing real branches and PRs — reshaping the repository before you’ve read the change. katamari recovers the same reading order without touching the repo: press u in ktmr diff, and the diff’s hunks are grouped into ordered semantic units — foundations and refactors first, then the code built on them, tests and docs with the change they cover — each one reviewable as its own scoped diff. The grouping is derived and read-only: no branches, no rebase, nothing written outside .katamari/.

The grouping runs through an agent CLI you already have — claude or codex, spawned headlessly (claude -p --output-format json / codex exec) under the account it’s already authenticated with. katamari deliberately has no LLM client and no API key of its own. With neither CLI installed, u says so in the status bar and everything else keeps working; ktmr doctor’s agents section shows which CLIs it can see and which one u would spawn.

What the model sees, and what it’s allowed to decide, is deliberately narrow:

  • katamari does the deterministic part first: every hunk gets a stable content-hash ID, and lockfile/generated noise (*.lock, package-lock.json, go.sum, *.pb.go, generated/ path segments, …) is split off before the model ever sees the diff, into a trailing “Lockfiles & generated” unit — the part of the diff a reviewer most wants to skip past.
  • The prompt carries each remaining hunk’s file path, add/remove counts, hunk header, and its changed lines only — never unchanged context, never whole files, and nothing at all from the noise bucket. A huge diff degrades deterministically (fewer excerpt lines per hunk) to stay inside a fixed prompt budget.
  • The model returns only a mapping — unit labels, one-line descriptions, an ordering, and which hunk IDs belong to each — never restated diff content. katamari then enforces the invariants the UI relies on: a hallucinated ID is dropped, an ID claimed twice keeps its first placement, and every hunk the model didn’t place lands in a trailing “Ungrouped” unit — so the units always cover the whole diff, exactly once.

The units panel (u) lists the units in reading order, each with its label, hunk count, and the files it touches; j/k select, and the panel’s bottom row shows the selected unit’s one-line description. Enter scopes the diff to that unit: the changed-file list and search narrow to just its hunks, and a two-row banner plus the status bar pin unit 2/5: <label> above the diff, so the scope is never ambiguous on screen. Esc widens back to the full diff (that takes precedence over its search-highlight-clear role), u reopens the panel with the current unit preselected — stepping unit-by-unit is two keys — and U discards the cached grouping and asks the agent afresh. Live refresh keeps working while scoped: units re-anchor to the refreshed diff by content, not line numbers, and a unit whose hunks were all rewritten away widens back to the full diff rather than stranding you on an empty view. Switching scope (working tree ↔ staged ↔ a revision) drops the unit filter — a grouping describes one diff, not the new one.

Generation runs in the background: the status bar shows units: asking the agent CLI … and the whole TUI stays interactive — expect seconds to minutes depending on the model, with a hard 3-minute cap, and a result that arrives after the diff has already changed is discarded with a status note rather than applied to the wrong diff. Results are cached in .katamari/groups.jsonl (covered by .katamari/‘s own generated .gitignore), keyed by the hunks’ content: reopening the same diff is instant and never prompts, while any edit to any changed line produces a new key, so a stale grouping is never shown against a diff it doesn’t describe.

The first u that would actually spawn a CLI — no [units] config anywhere, no cached grouping — opens a one-time three-step picker instead: which CLI, which model, which reasoning effort. The choice is appended as a [units] block to ~/.config/katamari/config.toml (append-only — anything hand-written in the file is preserved) and never asked again; Esc abandons it without saving or spawning anything. See [units] under Configuration for the keys and their exact CLI-flag mapping, and ktmr reset --units-config (Reset) to remove the block and get the picker back.