Specification
Formats, algorithms and invariants of Zit 0.1.
Normative for version 0.1. The reasons behind each part are recorded as architecture decision records (ADR 1 to ADR 17) in the adr/ folder of the repository. Each rule names the test that enforces it where one exists.
1. Objects
1.1 State
A git tree object. state id = tree id.
1.2 Change
A git commit object.
| Commit field | Meaning |
|---|---|
| tree | resulting state |
| parents | changes built on. First parent: the base. Second parent, if any: a change composed in |
| author name | agent |
| message | intent, then the author’s account, then trailers |
Trailers, one per line, after the intent:
Zit-Agent: <name>
Zit-Session: <id> (optional)
Zit-Read: <resource> (zero or more)
Zit-Tokens: <in> in, <out> out (optional: what the agent reported)
Zit-Cost-USD: <dollars> (optional)
Zit-Change: <commit id> (on a linear compose: the change it lands)
The committer is the person running Zit (user.name, user.email), and the commit is signed when commit.gpgsign is set. (tests/teams.rs, tests/usage.rs)
The first paragraph of the message is the intent; any further paragraphs before the trailers are the account: what the author reported doing and why (summary). A commit with no trailers is a valid change: agent = author name, intent = subject, account = body, no declared reads. (a_plain_git_commit_is_a_change)
1.3 Resource
resource = path ; whole file
| path "#" name ; top-level symbol
| path "#" type "::" m ; a method of a type
| path "#(imports)" ; the file's imports
| path "#" ; module-level code outside symbols
The path is everything before the first #; in it, a literal # is written %23 and % is written %25. (hashes_in_paths_and_names_round_trip)
Two resources overlap iff they are equal, or one is a whole file and both have the same path. For claims only, a type also holds its methods (T and T::m).
1.4 Evidence
A JSON blob:
{ "check": "ui", "key": "9f2c…", "state": "<tree id>", "passed": true,
"exit_code": 0, "duration_ms": 412, "output": "<last 4000 bytes>", "at": 1791025806 }
key = sha256(name NUL run NUL inputs)[..16 hex], where inputs is, for each declared input path in sorted order, path NUL "<mode> <type> <object id>" NUL as printed by git ls-tree, or path NUL "-" NUL if absent. Input paths are normalised first (./ui/ is ui); a path containing .. is an error. With no declared inputs, inputs is the state id. (input_paths_are_normalised_before_they_are_looked_up)
2. Refs
| Ref | Value | Written by |
|---|---|---|
refs/zit/current |
commit | init, accept |
refs/zit/changes/<commit id> |
commit | record; deleted by accept, discard |
refs/zit/evidence/<key> |
blob | check, accept |
A clone trusts only the evidence it produced, listed in <git dir>/zit/evidence-produced/<key>, unless git config zit.trustFetchedEvidence true. A fresh result with the same verdict as an existing ref keeps the existing object. (tests/replicate.rs)
Invariant. refs/zit/current only moves by compare-and-swap from the value the accepting process observed. (racing_accepts_all_land_exactly_once)
3. Footprint
footprint(base, tip):
git diff-tree -r --no-renames base tip.- For each changed path, index the old and new blob into units: top-level symbols and methods (
Type::method), the imports ((imports)), and everything else (path#). Each unit has a hash of its text, a hash of its interface (its text without function bodies and comments), and the identifiers it mentions. Attributes and comments directly above an item belong to it. writes= units whose hash differs, added or removed;pathif either side has no grammar, is not UTF-8, is not a regular file, or exceeds 1 MiB.signatures= the written units whose interface hash differs.refs= identifiers mentioned by the new version of each written unit, excluding the identifier that declares it, and excluding members of values (x.get); members of imported modules (lib.price) count.reads= everyZit-Readof the commits inbase..tip.
conflicts(mine, theirs): for each resource w in theirs.writes: write-write if it overlaps one of mine.writes; else read-write if it overlaps one of mine.reads, or if w is in theirs.signatures and mine.refs names it (its name, or for T::m, its type T). Symmetrically, write-read. At accept, write-write on prose (Markdown) and on (imports) is left to the text merge; on generated paths it is ignored.
Symbol naming:
| Language | Symbols | Methods, as Type::method |
|---|---|---|
| Rust | every top-level *_item, macro_rules!; an impl block’s header joins its type |
functions in impl blocks |
| Python | def, class, decorated definitions, NAME = … |
functions in a class body |
| JavaScript, TypeScript, TSX | function, class, interface, type, enum, const/let/var declarators, and their exported forms |
method_definitions in a class body |
| Go | func, type, const, var |
methods, by receiver type |
| Markdown | each heading’s section, named by the heading text | — |
Markdown sections have no references. Text before the first heading is path#.
4. Accept
footprints conflict compares footprint(merge-base, change) with footprint(merge-base, current), then drops conflicts on paths declared as [[derive]] and write-write conflicts on Markdown files and on (imports). git merge-tree clean? ignores conflicts inside [[derive]] paths. (ADR 12)
4.1 zit.toml
| Key | Meaning |
|---|---|
[[check]] name, run, inputs |
a check; see zit.toml |
[[check]] timeout |
seconds before the check is stopped and failed; default 1800 |
[[derive]] path, run |
a generated file, rebuilt on compose |
[prepare] run, inputs |
dependency install, run in the cached checkout, keyed by inputs |
ignore |
extra gitignore patterns for every workspace |
[accept] linear |
compose without merge commits |
Unknown keys are an error. The config is read from the state being acted on: at accept, checks come from current and from the composed state, derive from the composed state, and [accept] from current; prepare and ignore from the materialised state.
5. Workspace
Directory $ZIT_HOME/<repo>-<hash>/ws/<id>/, described in ADR 5. record:
git add -Aandgit write-treeinside the workspace view.- If the tree equals the base state and no compose parent is set: nothing to record.
- Otherwise create the commit, create
refs/zit/changes/<id>, and re-base the workspace onto the new change.
.gitignore applies: ignored files are not part of the state. (ignored_files_are_not_part_of_the_state) So do the user’s global excludes, built-in by-product patterns and zit.toml ignore, through git/zit-ignore. (tool_byproducts_are_never_recorded)
Materialising: clone the cached checkout of the state (or the newest cached checkout, moved to the state with read-tree -m -u, which keeps ignored files). If [prepare] is declared and the checkout’s .zit-prepared key differs, run it; it must leave diff-files and untracked files empty. Publish the result to the cache when it is a new state of current, a first checkout, or newly prepared. (tests/prepare.rs)
5.1 Claims
claim(workspace, resources), under one lock:
- Held = for every other open workspace, its claims and its in-flight writes; for every speculative change, its writes.
[[derive]]paths are never held. In-flight writes are reused for up to 2 seconds per workspace (inflight.json, keyed by base) and computed before the lock. - If any requested resource overlaps anything held: refused, nothing recorded, the holders returned.
- Otherwise the resources are appended to the workspace’s
claimsfile.
In-flight writes are footprint(base state, snapshot), where the snapshot is taken with a temporary index so the workspace’s own index is untouched. (tests/claims.rs)
5.2 Verification views
$ZIT_HOME/<repo>-<hash>/verify/<n>/, n in 0..8, each guarded by an exclusive lock on <n>.lock. Moving a view to a change: git reset --hard <change>, git clean -fd, empty its tmp/. (ADR 10)
6. Interfaces
6.0 Names
The command is zit; git-zit is the same program for git zit …. Refs are under refs/zit/, change trailers are Zit-Agent, Zit-Session, Zit-Read, Zit-Tokens, Zit-Cost-USD, Zit-Change, the config is zit.toml, local state is in $ZIT_HOME (default ~/.zit).
6.1 CLI exit codes
| Code | Meaning |
|---|---|
| 0 | done |
| 1 | a negative answer: accept rejected, a check failed, a claim refused |
| 2 | an error: not a repository, not initialised, unknown id, bad usage |
| 124 | zit run --timeout expired |
| other | zit run: the agent’s exit code, or 128 + signal |
Every command accepts --json.
6.2 Environment
| Variable | Effect |
|---|---|
ZIT_HOME |
where workspaces and caches live (default ~/.zit) |
ZIT_GIT |
git binary (default git) |
ZIT_MATERIALISE=checkout |
disable copy-on-write clones |
ZIT_AGENT |
default agent name |
ZIT_WORKSPACE |
set by zit run for the agent; default workspace for record, read and claim |
TMPDIR |
set for agents and checks: a private directory inside the workspace’s directory |
ZIT_SUMMARY_FILE |
set by zit run: where the agent may write its account; otherwise the end of its output is used |
ZIT_GH |
the GitHub CLI used by export --pr; default gh |
ZIT_CACHE_DIR |
set for agents and checks: $ZIT_HOME/<repo>-<hash>/shared |
ZIT_TRACE |
print each git call and its duration to stderr (batch read sessions excluded), and workspaces whose edits cannot be seen |
ZIT_CHANGE, ZIT_STATE |
set for check commands |
6.3 MCP tools
zit_materialise, zit_claim, zit_read, zit_record, zit_status, zit_show, zit_check, zit_retry, zit_dispose; with --integrator, also zit_accept and zit_discard. Arguments and results are the CLI’s --json shapes. A tool failure is a result with isError: true; an unknown tool or method is a JSON-RPC error. (tests/mcp.rs)
6.4 Web view
zit web serves, on the loopback interface, to Host: localhost|127.0.0.1|[::1] only, GET only:
| Path | Returns |
|---|---|
/ |
the page |
/api/graph |
{repo, current, mainline: [change…], changes: [change + status + fork + wrote…], workspaces: [workspace + dirty + writes + claims + overlaps + fork…]} |
/api/change/<id> |
the show --json shape plus diff |
/fonts/autohand-sans.woff2, /fonts/autohand-mono.woff2 |
the page’s fonts |
(tests/web.rs)
6.5 Rust library
cargo doc --open. The modules are the primitives: git (store), change, footprint, resource, symbols, workspace, claim, evidence, accept, clean, plus api (the shapes every interface returns), view, run, mcp, tui and web.