- Rust 98.4%
- Nix 1.6%
| docs/agents | ||
| src | ||
| tests | ||
| .gitignore | ||
| AGENTS.md | ||
| Cargo.lock | ||
| Cargo.toml | ||
| CONTEXT.md | ||
| flake.lock | ||
| flake.nix | ||
| README.md | ||
| tatami.toml.example | ||
tatami
A pool manager for jujutsu workspaces. Tatami keeps a pool of isolated, reusable working copies ready for agents and developers to pick up instantly, with build caches kept warm between uses.
Starting a fresh workspace per agent session throws away installed dependencies and build caches every time. Tatami manages a pool of pre-warmed slots so each session gets its own isolated jj workspace instantly, and returns it to the pool warm when done.
It is a jj-native reimagining of treehouse: where treehouse pools git worktrees, tatami pools jj workspaces and adds bookmarks as durable work identity. Linux only.
Quick start
cd myproject # any jj repo
tatami # acquire an anonymous slot, drop into a subshell
# ... work inside the warm workspace ...
exit # subshell exit returns the slot to the pool
tatami my-feature # acquire a slot bound to bookmark 'my-feature'
tatami status # show the pool
Installation
Nix only.
# run without installing
nix run github:weiwen/tatami
# install into your profile
nix profile install github:weiwen/tatami
# hack on it
nix develop # dev shell with the rust toolchain + jj
Requires jj on PATH at runtime (bundled in the dev shell).
How it works
- jj workspaces are the isolation primitive (
jj workspace add). Git is not supported. - Return resets, never deletes (
jj new trunk()), preservingtarget/,node_modules/, and other build artifacts for the next user. - No dirty check: a jj working copy is always a commit, so any change is already in
the bookmark's history. The "merged into
trunk()" check covers reclamation safety. - No daemon: pool state is a small JSON file per pool, written atomically under a
file lock. In-use detection scans
/procfor processes whose cwd is inside a slot. - Self-healing: a crashed session's owner reservation is cleared automatically, and a corrupt state file is rebuilt from the slots on disk.
Vocabulary
- Pool — the managed slots for one repository (one pool per repo).
- Pool slot — an anonymous, reusable directory holding one jj workspace; working copy and build cache persist across uses.
- Bookmark — your named line of work.
tatami get my-featurebinds bookmarkmy-featurein a slot; it persists after you return the slot and resumes in a fresh warm slot on the nexttatami get my-feature. - Lease — a durable reservation that survives with no process running in the slot.
- Trunk — jj's
trunk()revset; the reset target when a slot returns to the pool.
See CONTEXT.md for the full domain model.
CLI reference
| Command | Description |
|---|---|
tatami [get] [bookmark] |
Acquire a slot and open a subshell. get is implicit, so tatami feat is the same as tatami get feat. Pass a bookmark to bind durable work identity. |
tatami enter <index|bookmark|path> |
Open a subshell in an existing slot without acquiring or resetting it (alias cd). |
tatami return [path|index] |
Release a slot, terminate lingering processes, and return it to the pool. Defaults to the current workspace. |
tatami status |
Show pool status. |
tatami prune |
Reclaim stale slots whose work has landed on trunk (dry-run by default). |
tatami destroy <path|index>... |
Deliberately remove one or more slots, or a whole pool (dry-run by default). |
tatami init |
Write a starter tatami.toml in the repository root. |
get
tatami [BOOKMARK] [--lease] [--lease-holder <LABEL>]
get is implicit: any first argument that is not a known subcommand is treated as a
bookmark, so tatami feat is shorthand for tatami get feat.
--lease— durably lease the slot without a subshell; prints only the slot path to stdout, so it composes with scripts:path=$(tatami get my-feature --lease) # ... use "$path" as a durable home; no process needs to stay inside it ... tatami return "$path"--lease-holder <LABEL>— label recorded as the lease holder (defaults to$TATAMI_LEASE_HOLDER).
prune
tatami prune [--all] [--yes] [--verbose]
--all(alias--global) — sweep every managed pool under the user-level tatami root.--yes— delete the candidates instead of doing a dry run.--verbose,-v— list each skipped slot with its reason.
destroy
tatami destroy <PATH|INDEX>... [--all] [--yes] [--include-unlanded] [--include-in-use] [--include-leased]
Both destroy and return accept a slot's index (the number shown by tatami status)
in place of its path; a bare number is resolved against the current repository's pool.
destroy accepts several slots at once (e.g. tatami destroy 1 3 5), all within one
pool; --all still takes a single pool or repository.
--all— target every slot in the named pool (PATH is a pool path or repository).--yes— execute the removal instead of doing a dry run.--include-unlanded— also remove slots whose bookmark has not landed on trunk.--include-in-use— also remove in-use slots (their processes are terminated first).--include-leased— also remove a leased slot (only when naming the exact slot path).
Structured output
Every command accepts a global --json flag, or --toon for the same result encoded
as TOON (a token-efficient JSON alternative for
LLM prompts). The two are mutually exclusive:
tatami status --json
tatami get my-feature --lease --json
tatami prune --all --toon
The contract is strict so a consumer can parse stdout unconditionally:
- On success,
stdoutcarries exactly one document (the command's result). - On failure,
stdoutcarries exactly one{"error": "..."}document and the exit code is non-zero. - All human progress and status chatter is suppressed; nothing but the document is written to either stream (hook output, being your own scripts', is left untouched).
tatami destroy in a structured mode reports a single-slot no-op through the skipped
array (each entry lists the needs flags) and exits zero, rather than the non-zero exit
the text mode uses. The interactive tatami get subshell emits its acquired-slot
document, then hands stdout to the subshell; use get --lease --json (or --toon)
for a non-interactive flow.
Configuration
Repo-level tatami.toml (see tatami.toml.example) holds only safe settings:
# Maximum number of slots in the pool.
max_slots = 16
# Pool root override. Empty uses $HOME/.tatami.
# Relative paths resolve from the repo root; a ".tatami" component is appended.
# root = "$HOME/.local/share/tatami/"
Lifecycle hooks are read only from the user-level ~/.config/tatami/config.toml,
never from a repo-level file, so cloning a repository can never make tatami run someone
else's commands:
[hooks]
post_create = ["./scripts/setup.sh"]
pre_destroy = ["./scripts/teardown.sh"]
The user-level config may also set max_slots and root; a repo-level tatami.toml
overrides those but never contributes hooks. $VAR / ${VAR} references in root
expand from the process environment.
Suggested jj config
Idle slots keep can clutter jj log. This revset hides idle tatami-
workspaces (add it to your jj config, e.g.
~/.config/jj/config.toml):
[revsets]
log = "(present(@) | ancestors(immutable_heads().., 2) | trunk()) ~ bookmarks('tatami-')"
Development
cargo build
cargo test
cargo clippy --all-targets -- -D warnings