CLI reference
tama install
Installs Tama-managed user-global Git dispatchers for pre-commit, commit-msg, pre-push and reference-transaction, preserving each pre-existing unmanaged hook once as .before-tama.
Every push ends at tama hook review-push, which reads the attestation each pushed commit carries in its own message: every claim of the commit standard, word for word (tama attestation standard --commit prints the block to carry). A commit whose message lacks a claim refuses the push with review-push: BLOCKED: <local ref> -> <remote ref>: commit <sha> carries no commit attestation in its message: and one missing: "<claim>" line per missing claim; the dispatcher adds tama: the review refused this push of <repository>. No model, key, Brama or other product is asked, so a push is reviewed the same on a machine with nothing but Git and Tama. The real journey tests/review/attested.rs pushes through Git to a bare remote with Brama's address removed and no resolver state, requires the commit without the attestation refused and the remote unchanged, then the same commit carrying the printed block pushed.
Before every push, repo-githooks/rust/pre-push invokes
tama hook check-rust-format. For each non-deleted pushed ref it
takes the Rust files the push changes — those git diff --name-only
<remote's current commit> <pushed commit> lists as added,
modified or renamed, or every Rust file of the commit when the remote ref is
new or its commit was never fetched — writes the pushed commit's tree out
with a private Git index (read-tree/checkout-index)
under the checkout's ignored .build/tama-rust-format/ directory,
and runs rustfmt --check --edition <edition> <file> on
each file there. Its edition comes from the committed package declaring that
target; modules under an explicit target's directory retain that owner before
an enclosing package is considered, and a file a target declares through
#[path = "…"], or that such a file declares in turn, belongs to that
target wherever it lies. Otherwise the nearest containing package
supplies the edition. Workspace inheritance follows the committed declaration,
and conflicting owners refuse instead of choosing an arbitrary edition.
The snapshot is then removed. A push that changes no Rust file needs
no check. This works without a Wisent release manifest or Stado
installation. Declare .build/ in the repository's ignore policy;
the gate refuses non-ignored scratch storage. No clone, branch checkout or
change to the caller's index is made.
Running rustfmt is running a build tool, and the operator's rulings decide
when one may run. Before it formats anything, the gate asks Oko's one admission
decision, oko admit build --product <checkout directory>
--json, and formats only when Oko admits the build: an unscoped
consent, or no ruling and Oko's open-work rule satisfied. Otherwise it runs no
formatter, lets the push through and prints why: check-rust-format: no
formatter was run: <Oko's verdict quoting the ruling> under a
standing withdrawal (no-build, no-build:<product>),
… Oko does not admit a build of <checkout> yet: <what it waits
on> while Oko's open-work rule holds building, and … whether a
build of <checkout> is admitted could not be read, so none runs:
<cause> when oko admit cannot answer or the
installed oko predates it. No push is refused for formatting
while formatting is not admitted. A push names no purpose, so a consent the
operator scoped to a defect's repair (build@<defect>) does
not cover it; a later unscoped consent does, and the gate runs again. The
managed quality-check (stado quality check
--pre-push) asks the same before it starts Stado and prints
quality-check: no quality gate was run: … with the same reasons.
Machines with no oko installed run both gates. The real journey
is tests/build-gate/pre_push.rs.
The formatter's file diffs and concrete failure reach Git's stderr. The working tree is not read: formatting that exists only uncommitted cannot qualify an unformatted commit, and another session's uncommitted edit in the same checkout cannot refuse a formatted one. A file the push does not change is not read either: a file already unformatted on the remote holds back only the push that changes it, whose author formats it as part of that change — never a push that leaves it alone, and never through a reformat-only commit of someone else's file, which the commit review refuses. Any pushed revision is checked, whether or not it is checked out. The gate changes no working source files. A missing committed root formatter configuration uses rustfmt's built-in defaults within the snapshot, never an uncommitted ancestor configuration. A file with no committed package or explicit target owner is refused by name. A package omitting its edition uses Cargo's documented edition default; missing inherited workspace declarations remain errors.
On every push the dispatcher then runs tama hook quality-check
with Git's pre-push records on its stdin, which it hands to the installed
stado quality check --pre-push --if-declared: Stado reads the
.wisent-release.json each pushed commit carries and runs that
commit's declared fmt gate on the commit's own tree. The working
tree is never read and never chooses whether the check runs: a repository has
one checkout that several sessions edit at once, so another session's
uncommitted file neither refuses a finished commit nor lets an unformatted one
through. A commit that declares no manifest, no formatting gate, or
releases:false is Stado's own answer (… declares no
formatting gate for platform …; nothing to check, exit 0); the
dispatcher does not read the manifest's text itself, so a gate is never skipped
because the file spells it another way. A manifest Stado cannot read or parse
refuses the push with Stado's cause. A Stado too old to know
--pre-push refuses every push with its argument error until it is
updated. review-push follows the formatting checks. A release
install writes the same dispatcher. Graphical Git clients invoke the same hook
and receive the same refusal and file diagnostics.
A failed stado quality check can name an unavailable release input or another dependency, not just a formatting defect. tama hook quality-check passes everything Stado printed through and then refuses in Stado's own terms. A formatting gate's refusal — the one Stado marks with `stado quality format` writes what it reads — ends with quality-check: stado quality check refused this push of <checkout>: a formatting gate refuses the committed tree (its report is above); run `stado quality format` there, commit the result and push again. Any other refusal repeats Stado's failure line: quality-check: stado quality check refused this push of <checkout> at <failure_point> [<error_code>]: <detail>; this is not a formatting finding, so `stado quality format` changes nothing for it; repair that operation and push again — for a release input Skarbiec could not serve, at cli.quality.check [infra_down] with the item and field Stado named. A refusal Stado wrote no failure line for says so, with Stado's exit status. Resolve the reported cause before another push; only a formatting gate's own diff justifies formatting the source. The real journey tests/quality-check/refusal.rs runs the program through the installed Stado on an unformatted committed tree, on a committed Cargo.lock that does not resolve its manifest, and on a tree the check passes.
On a machine without Stado installed (no ~/.stado/bin/stado), tama hook
quality-check reads the pushed commits from the same pre-push records, writes each one's tree
out under the checkout's ignored .build/tama-quality/ through a private index, reads that
commit's manifest there and runs each gate it declares for this platform
(platforms.<platform>.quality, by name and argv) in that tree, its output passed
through, saying quality-check: Stado is not installed; running the declared gate <name>
(<argv>) on commit <commit> before each; the first that does not pass refuses with
quality-check: the declared gate <name> refused this push: commit <commit> does not pass
it (<exit>); its output is above; repair what it reports and push again, and the gates after it
do not run. The written-out tree is removed after each commit; a build directory Git does not ignore, or
a tree that cannot be written or removed, refuses by its cause. A commit that declares no manifest, no
entry for this platform or no gate passes, saying so; a manifest that is there and cannot be read, or a
gate without a name, without an argv or with an empty one, refuses by its cause, since nothing can then
say what the gates are. The same absence lets block-internal-names judge by this account's
own names, saying that no fleet registry and no vault listing are here, instead of refusing every write
for a reader that is absent; a Stado that is installed and cannot answer stays a refusal by its cause.
Real journeys tests/review/quality/declared.rs (real pushes to an isolated bare remote,
judged by a real rustfmt --check with unformatted and formatted uncommitted work beside the
commits) and tests/internal-names/without_stado.rs, each with a HOME holding no
stado.
The real external_target_modules_keep_the_declaring_package_edition journey runs the formatter hook against an isolated Git tree containing a root package and an external target with a different edition. It requires the external module to retain its declared edition despite a contradictory working manifest, then requires a committed formatting defect to name that module and edition. It retains source/binary identity and command results; the separate installed push journeys qualify review and Git LFS integration.
#Review before creating a commit
A commit is made only with its attestation. tama hook review-commit reads the message Git will record for every claim tama attestation standard --commit prints, word for word, exactly as review-push reads it later, and without a model. A message that lacks a claim refuses the commit with the message carries no commit attestation and one missing: line per claim, and no commit is made, so no unreviewed commit can pile up under later work and hold every push behind it. On a machine that declared no model (tama model-provider set --none) the attestation is the whole review and no reviewer is asked. The real journey tests/commit-review/attested.rs commits through Git with the built hook and proves the unattested commit leaves the branch where it was and the attested one moves it.
Before any model is asked, tama hook review-commit reads the
message Git will record (its # comment lines dropped) with the
same rule block-internal-names applies to files: a fleet host,
account or vault item name anywhere, or a date or a hex id of seven or more digits whose letters share one case (a commit, defect, task or session id) in its prose;
a run that mixes cases, or is joined into a longer name (a code path segment such as crypto::…::verify, the first word of a hyphenated crate name), is a name. A
message is published with its repository, so any such detail refuses the
commit with the commit message would carry internal details, one
line per value, and the incident it came from goes to Oko instead. This
refusal needs no reviewer, works while Brama is unreachable and retains no
review. A source of names that cannot be read (the Stado registry, the vault
listing, the product declarations) stops the commit naming that source; it
never lets a message through unchecked.
The managed pre-commit dispatcher allows one commit at a time per checkout, because Git keeps one message file per repository whichever index a commit is made from: tama hook commit-lock.
The managed commit-msg dispatcher runs the preserved hook chain
first, then tama hook review-commit <message-file>. It
compiles nothing: the agent that wrote the change reads it, and a commit that
does not build is that agent's failure, not a gate's. Graphical Git clients
invoke the same dispatcher and receive the same stderr. On a machine that
declares a model, Brama
reads the final message and exact staged patch under the same message and
change-unit policy: private incident identifiers stay
out of messages, messages explain the product change, and formatting-only or
rename-only corrections belong in the change they complete. A formatting
capability is not rejected merely because its message mentions formatting.
The refusal names the offending text copied from the actual message or
staged patch. An unavailable reviewer, malformed finding or quote absent from
that source refuses as a failed review operation, never as a proven source
defect. A staged tree or message changed during review is not authorized.
Git creates no commit on refusal; graphical Git clients receive the same
stderr. The operation prints czekam while the real reviewer owns
the request, until the reviewer answers. tama reviews list/show
and Desktop Reviews expose its retained envelope, answer and outcome through
the shared review reader. Install plan and hook status include
commit-msg; neither wiring nor a source commit proves a real
commit was accepted or refused.
Session-owned staging is qualified separately through tests/shared-checkout/commit.rs, using retained edit receipts and actual Git trees. See tama stage for the capture requirements, overlap refusals and preserved-index checks. A staged tree is not a compilation verdict.
A grounded commit refusal prints review retained at <directory>, just as a failed review operation does. Inspect that record's result.json to distinguish source_finding from review_failure; the quoted offending text belongs to the retained finding. The real managed-commit journey checks this outcome and unchanged Git HEAD, rather than treating an incidental stderr sentence as proof that policy ran.
See Testing and qualification for the real commit-review journey and its retained evidence contract.
#Example: a registered test needs formatting
If a push reports tests/harnesses/payload.rs (edition 2021)
with the formatter's diff, fold the formatting into the unpublished commits
that wrote it with tama format fold,
which keeps each commit's message, author and attestation note and leaves the
index and working tree as someone left them, then push again:
tama format fold
git push
check-rust-format: push refused: commit <commit>: N Rust file(s)
this push changes are not in the form rustfmt writes: followed by each
file, its edition and the formatter's diff identify the pushed commit and its
files. Other refusals name an unreadable manifest, a file with no committed
edition above it, an unresolved revision, a failed Git export operation, an
unignored scratch path or a cleanup failure. A cleanup failure is retained
alongside a formatter failure. Fix that cause; the remote branch is not
updated by a refused push.
The real journeys tests/review/format.rs and tests/review/installed.mjs push through actual Git, Cargo/rustfmt, the managed dispatcher and the real review and LFS dependencies. They check that uncommitted formatting cannot repair an unformatted commit, that a formatted commit can be pushed while another session's source and invalid formatter configuration remain unchanged, and that a file left unformatted on the remote before the dispatchers were installed refuses only the push that changes it, naming that file alone, while a push that leaves it alone reaches the remote. Remote refs and transferred LFS bytes establish the final state; command logs and source identities are retained.
cargo test --manifest-path rust/Cargo.toml -p tama-cli --test review_format -- --ignored --nocapture
node --test tests/review/installed.mjs
Set TAMA_TEST_SOURCE_REVISION to the exact committed candidate for both journeys; each refuses a different revision or edited tracked source before fixture pushes. The Cargo journey retains the executable digest. The installed journey retains the actual sealed runtime identity and requires its revision to match that same declared candidate. Both require Git LFS; unavailable real dependencies are failures, not skipped passes. Each fixture commit carries the commit attestation, so review-push lets through what the formatting gate passes.
The remote-held formatting case lives in
tests/review/format/remote.rs and is registered by the same
review_format target. It shares the declared-candidate and executable
identity preflight; recording whichever revision happens to be checked out is
not sufficient qualification. Both cases retain identity before fixture pushes
and refuse dirty tracked source.
Each changed Rust file is formatted with child-module traversal disabled.
An unchanged mod declaration cannot pull a remote-held formatting
defect into an unrelated push. Changed module files are still checked
individually from the exact committed changed-path list. The real remote-held
journey includes an unformatted child module beside the formatted parent it
changes.
Formatter editions come from the exported commit's Cargo declarations, including explicitly registered tests outside their package directory and workspace-inherited editions. The manifest index is read once per candidate; no dependency resolution, network request or working-tree manifest is used. External target modules use their nearest declared crate-root directory, and conflicting owning editions refuse with the candidate file and manifest paths. Missing target paths, unreadable manifests and unresolved inheritance remain causal refusals, not a guessed edition. A package omitting its edition follows Cargo's documented default. The real formatter journeys cover an external registered test and inherited workspace edition. Removing a declared target's real source then requires a causal formatter refusal and an unchanged remote ref.
A declaration, source-read or formatter-execution failure is not a verdict that the Rust text needs reformatting. The refusal names the failed operation and directs repair there; only actual formatting findings ask for formatting folded into the unpublished source change.
#What review-push reads
For every pushed ref, review-push lists the commits the push adds, oldest first: those reachable from the pushed commit and not from the remote's current commit, or, for a ref the remote does not hold yet, not from any ref Git keeps for that remote. It reads each one's whole message from Git and refuses at the first that lacks a claim; the tip's message speaks for no commit under it. A deleted ref is not reviewed. A pushed commit Git cannot list or read refuses with review-push: the pushed commits cannot be read: <local ref> -> <remote ref>: <git's cause>; a line Git hands over in another shape refuses as unreadable pre-push line. Before pushing, tama attestation check --commit REV or Tama Desktop's Carried attestations section gives the same verdict for one commit.
#What review-ref-move reads
Every move of a ref passes tama hook review-ref-move from the reference-transaction dispatcher, with Git's own input: the transaction state as the operand and one <old> <new> <ref> line per ref. Only a local branch (refs/heads/…) whose old tip is not under the new one is read — a rewrite or a reset: git rebase, filter-branch, commit --amend, reset --hard. A commit, a merge, a fetch or a push adds commits and is not read. For every commit such a move drops that no remote holds, the defect register is asked oko defects naming <repository>@<commit> whether an open defect records it as its found-in or repair.
At the prepared state, a named commit whose patch no commit under the new tip carries (same author instant, same git patch-id --stable, read from every commit the move adds, the new tip included, whatever its committer date — an amend or a rebase dates its copies at the moment it ran, so a message-only amend carries the patch) refuses the move: review-ref-move: BLOCKED: refs/heads/<branch>: the move drops <sha>, which the register names and no commit under the new tip carries as the same patch: followed by one <defect> (<product>) found-in|repaired <repository>@<sha> line per entry, then the instruction to keep the patch in the rewritten history or record the replacing revision first; the dispatcher adds tama: the review refused this move of a branch of <repository>, Git aborts the transaction and the history stays. A named commit whose patch the new history carries under a new id passes, and at the committed state oko defects rebased --apply moves every entry to that id, each line it prints relayed as review-ref-move: <defect> repaired <old> is now recorded as <new>. A move that drops no recorded commit passes without a word. A machine without an oko program has no register to ask: the move passes with review-ref-move: no oko on this machine; refs/heads/<branch> moves with no register asked. An Oko that fails to answer, a ref line in another shape or a Git command that fails refuses with review-ref-move: the moved refs cannot be read: <cause>. Real journey: tests/review/ref_move.rs.
A named commit is also carried when a commit under the new tip, or the new tip itself, carries a rewrite record naming it: a note under refs/notes/rewritten listing the ids it stands for, one per line, which tama format fold leaves because formatting a commit changes its patch and a commit that only formatted is dropped. The review says review-ref-move: <commit> is carried by <commit>, whose rewrite record names it, and oko defects rebased --apply follows the register's revision to that commit through the same record.
A bare repository has no checkout for the register to name. A fetch that moves a mirror's branch to a remote's rewritten history (a package manager's cache, a clone Stado makes to bundle a revision) drops commits that are the remote's, not unpublished work, so the move passes with review-ref-move: refs/heads/<branch> moves in a bare repository, which no register entry names. Real journey: a_bare_mirror_follows_a_rewritten_branch_through_the_review in tests/review/ref_move.rs.
#Invocation
tama install [--git-hooks-path <path>] [--home <path>]
[--set-git-config] [--no-git-config] [--json]
--git-hooks-path chooses the dispatcher directory. Otherwise Tama uses global core.hooksPath, falling back to ~/.config/git/hooks. --home controls home expansion and the Git configuration environment. --set-git-config writes global core.hooksPath; --no-git-config wins when both flags are present. --json selects structured output.
#Output and state
Tama creates the hooks directory, writes executable managed dispatchers, and may preserve pre-commit.before-tama, commit-msg.before-tama, pre-push.before-tama and reference-transaction.before-tama. Output names the resolved hooks path, every installed target and whether Git global configuration changed. Shared history that the defect register names is kept by review-ref-move; the commit-time guard prevents new invalid messages from entering it.
#Git editors in agent sessions
The OMP adapter obtains process-scoped Git configuration from
tama hook pre-bash --git-environment during activation. That
local Rust operation preserves existing Git environment pairs and sets
core.editor and sequence.editor to the running
Tama binary's hook pre-bash --git-editor refusal program.
It does not change the human's global Git configuration or silently open
an editor. Inherited GIT_EDITOR and
GIT_SEQUENCE_EDITOR overrides are removed from the agent
process; explicit per-command noninteractive editors remain possible.
The same contract sets core.pager to cat, the
value for which Git launches no pager, and names it as
corePager. An agent's shell has nobody to press a key, so a pager
started there would hold the session's call open for good. Configuration beats
an inherited PAGER; an explicit GIT_PAGER or
git -p still decides, and git -p is refused by
pre-bash. The real interactive_git_refusal journey
an_agent_process_launches_no_git_pager asks actual Git which
pager it would launch (git var GIT_PAGER) with
PAGER=less, without and with the contract, and requires
less and then cat.
Malformed counts, missing key/value pairs, executable lookup failures
and malformed configuration replies retain their actual cause. The adapter
reports gitEditors in runtime status and refuses tool calls
when configuration failed; it does not drop all hooks by failing extension
registration. The Rust command prints a set/remove
environment contract for other agent launchers. Merely reading that contract
does not configure a launcher: it must apply it before starting tools.
Prepare messages with git commit -F message-file or
git commit -m 'Describe the change'; preserve an existing message
with --no-edit. For an authorized history rewrite, prepare the
todo file and use
GIT_SEQUENCE_EDITOR='cp prepared.todo' git rebase -i upstream.
Normal commit and push hooks still run. The real
interactive_git_refusal journey obtains the actual environment
contract, observes Git's effective editor settings, requires a message-less
commit and interactive rebase to refuse, and checks that a prepared-message
commit succeeds with the expected persisted message. Its Node consumer uses
the production adapter bridge and checks that a malformed environment leaves
the agent unchanged, inherited editor overrides are removed, and repeated
activation preserves unrelated configuration. The real sealed-release
tests/enforcement/live-reload.mjs journey separately requires
managed editor state in the actual OMP adoption receipt and persisted session.
#Release activation and connection status
tama hooks release connects the managed dispatchers to global
core.hooksPath after installing the sealed runtime. Publishing a
repo-githooks file alone does not connect it to Git. Activation
preserves an existing Git LFS hook as pre-push.before-tama; the
dispatcher replays the original push arguments and ref input to that hook after
formatting and review succeed. The sealed runtime's binaries lead its PATH. Because a dispatcher runs the hooks of whichever release is current, an install with --session-control-only rewrites every global dispatcher that is already Tama's for the release it activates, and connects no other.
Each dispatch stores Git's ref input in a private, uniquely allocated
directory under Tama state: ~/Library/Application Support/Tama/git-dispatch
on macOS, or $XDG_STATE_HOME/tama/git-dispatch on other Unix hosts
(~/.local/state/tama/git-dispatch when that variable is absent).
It does not use the system temporary directory. Normal completion, refusal,
and handled termination remove that dispatch's input and directory; allocation
or input-write failure refuses the Git operation before any chained hook runs.
The installed Git journey also supplies an absent TMPDIR
under its retained evidence directory. The push must still reach the actual
formatter's named-file refusal and leave the remote unchanged. This catches a
dispatcher that fails while creating a system-temp input spool, before its
consumer reads Git's refs. The command log retains that environment input.
Reviewer HTTP diagnostics report measured DNS lookup, TCP connection, TLS, first-response-byte and total transfer times, alongside the actual status and Brama cause. A fast readiness response is not evidence that the authenticated review request answered quickly, and a degraded gateway is not evidence that every route is unavailable. No inferred outage, successful process or empty model reply authorizes an unreviewed push. Malformed response JSON retains its parser cause; absent or non-text normalized content names the exact field.
tama hooks status
tama hooks status --home /path/to/home --json
tama hooks status --repo ~/code/my-service
Status reports the configured user-global directory, each dispatcher, whether the Rust gate is referenced and present, and the preserved original hook path. The same descriptions appear in Desktop's installation plan. This is a wiring inspection, not a successful push claim.
Git reads one hooks directory per repository, and a repository's own
core.hooksPath wins over the global one. A repository that sets it
runs that directory instead of the dispatcher, so the Rust format check, the
quality check and review-push never run on its commits and pushes,
and its pushes leave unreviewed without a word. --repo PATH reads
that repository's own setting: with none, it says Git runs the dispatcher and
exits 0; with one, it names the directory, says which gates do not
run and how to remove the override, and exits 1. JSON mode carries
it as repository (repository, hooksPath,
bypassesDispatcher, description) beside
userGlobalGit. A repository needs no hooks path of its own: the
dispatcher runs its .githooks/<hook> after its own gates. A
path that is not a Git work tree is refused with Git's own answer.
If the runtime installs but connecting Git fails, release activation refuses
and names that operation and its cause. If a foreign hook and its
.before-tama backup contain different content, installation refuses
instead of overwriting either version. Resolve that hook-chain conflict before
activating again. An emergency-disabled dispatcher remains protected from
implicit reactivation.
#Refusals and failures
Filesystem and process errors are written to stderr and exit 1. When requested Git configuration returns nonzero, Tama names the command, Git's exit status and Git's stderr: git config --global core.hooksPath <path> ended with <status>: <stderr>; a Git that cannot be started is cannot run git to set core.hooksPath: <cause>. Uninstall reports git config --global --unset core.hooksPath ended with … the same way. Without --set-git-config, the command deliberately leaves Git configuration untouched.
Return to the complete CLI command tree.